You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The wasm-policy crate ships a working, sandboxed wasmtime policy engine (fuel + memory + epoch/time limits, module compilation + export validation, LRU module cache, optional on-disk persistence). However, none of it is reachable by a client: there are no /policies REST routes, the rest-apiAppState has no PolicyEngine, and the UI Policies page was deliberately replaced with a ComingSoon placeholder during the 12-PR quality + hardening pass.
This was an intentional gating decision, not a regression. The gating landed in commit 63f8322 ("feat(rest-api): wire namespace + webhook management; gate hollow UI sections"), which wired up namespaces + webhooks but explicitly stubbed the Policies section rather than ship a half-wired path. This issue tracks completing that deferred work.
The engine, store, and UI client/types already exist and are honestly labelled — the missing piece is the REST backend plus the AppState wiring and re-enabling the page.
Current state (verified against live code)
Engine exists and is fully usable — crates/wasm-policy/src/lib.rs:80-86 re-exports PolicyEngine, PolicyStore, Policy, PolicyMetadata, PolicyId, ResourceLimits, etc.
WASM is validated on register: size cap (MAX_POLICY_SIZE = 1 MiB, lib.rs:120), compiled via Module::new, and exports checked via validate_exports — crates/wasm-policy/src/store.rs:76-88
No /policies routes registered. crates/rest-api/src/routes.rs:36-94 nests only /keys, /audit, /auth, /namespaces, /webhooks.
AppState has no policy engine field — crates/rest-api/src/middleware.rs:23-45 (fields: sessions, key_manager, rate_limiter, rbac, namespaces, acls, webhooks, audit, start_time).
hsm-rest-api's Cargo.toml does not depend on hsm-wasm-policy — crates/rest-api/Cargo.toml lists hsm-auth, hsm-audit, hsm-crypto-engine, hsm-key-manager, hsm-webhooks only (lines 22-26).
axum is pulled in with features = ["json", "macros"] — crates/rest-api/Cargo.toml:11. The multipart feature is not enabled, so multipart WASM upload extraction is not currently available.
The server binary builds AppState via AppState::with_key_manager(...).with_audit_logger(...) and calls create_router — crates/hsm-server/src/main.rs:346-350. A live PolicyEngine must be constructed here and threaded into AppState.
Namespace handler notes policy attachment is unwired — crates/rest-api/src/handlers.rs:1288 ("policy attachment is not yet wired").
UI is built against routes that 404.
ui/app/(protected)/policies/page.tsx:1-11 renders <ComingSoon ... detail="...uploading and managing policy modules over the REST API (multipart WASM upload + a live policy engine) is a future release." />.
ui/lib/api.ts:256-295 already implements listPolicies (GET /policies), createPolicy (multipart POST /policies, FormData), getPolicy, deletePolicy, attachPolicy (POST /policies/{id}/attach) — all currently hit a 404.
UI proxies /api/hsm/:path* → ${HSM_API_URL || http://localhost:8443}/:path* — ui/next.config.mjs:6-7, so route paths must match the client (/policies, not /api/policies).
What is needed
Add hsm-wasm-policy as a dependency of hsm-rest-api and enable axum's multipart feature (crates/rest-api/Cargo.toml).
Stand up a live PolicyEngine in AppState (crates/rest-api/src/middleware.rs): add a pub policies: Arc<PolicyEngine> field, construct it in new/with_key_manager (a PolicyStore::with_storage for persistence, or in-memory store, with sensible default ResourceLimits). Wire construction in crates/hsm-server/src/main.rs near line 346 so the deployed binary has it.
Add /policies routes to the authenticated router (crates/rest-api/src/routes.rs):
GET /policies → list (map PolicyStore::list_metadata → UI Policy shape).
POST /policies → multipart upload (fields: name, optional description, fuel_limit, memory_limit, and the wasm file). Build PolicyMetadata + Policy::new, call register_policy (which validates size + compiles + checks exports). Return the created Policy.
GET /policies/{id} → get one.
DELETE /policies/{id} → remove.
POST /policies/{id}/attach → attach to a namespace (body { namespace }); use the metadata namespace filter / PolicyStore::update.
Handlers in crates/rest-api/src/handlers.rs: enforce RBAC (policy management is privileged — gate behind the same permission tier as namespace/webhook management), surface compile/validation failures as 400, map PolicyError::NotFound → 404. Note multipart bodies bypass the JSON path but are still bounded by the existing DefaultBodyLimit (1 MiB, routes.rs:20) — confirm this is compatible with MAX_POLICY_SIZE (also 1 MiB) or raise the limit for this route.
Map the wire shape to the UI Policy interface (id, name, description?, wasm_hash, fuel_limit, memory_limit, created_at, attached_namespaces[]) — ui/lib/types.ts:225-234. wasm_hash ← metadata.bytecode_hash, attached_namespaces ← metadata.namespaces.
Re-enable the UI page: replace ui/app/(protected)/policies/page.tsxComingSoon with a real page that lists policies, uploads a .wasm module via the existing hsmApi.createPolicy(FormData), and supports delete + attach-to-namespace. The api.ts client is already written (ui/lib/api.ts:256-295) — no changes needed there beyond confirming paths.
Optionally wire policy evaluation into the signing path (call PolicyEngine::evaluate before performing a sign for a namespace with attached policies), or leave evaluation enforcement to a follow-up and scope this issue to management + storage only. State the chosen scope in the PR.
Acceptance criteria
hsm-rest-api depends on hsm-wasm-policy; axum multipart feature enabled.
AppState carries an Arc<PolicyEngine>, constructed in new/with_key_manager and wired through hsm-servermain.rs.
GET /policies, POST /policies (multipart), GET /policies/{id}, DELETE /policies/{id}, POST /policies/{id}/attach are registered under the authenticated router and protected by RBAC.
Uploading a valid .wasm module that exports evaluate succeeds and the policy appears in GET /policies; an invalid/non-compiling module returns 400 with a clear message; over-size returns 413/400.
Response bodies match the UI Policy type; hsmApi.listPolicies/createPolicy/getPolicy/deletePolicy/attachPolicy all return non-404.
ui/app/(protected)/policies/page.tsx no longer renders ComingSoon; it lists, uploads, deletes, and attaches policies against the live API.
Tests: route-level integration tests for register (valid + invalid WASM), list, get, delete, attach; preserve the existing body-size-limit test.
cargo clippy --all -- -D warnings, cargo test --all (skipping slow tests per CLAUDE.md), and cargo doc --no-deps --all are clean.
Related (do not duplicate)
Distinct from #3 (threshold-ECDSA), #4 (SNIP-12), #5 (Cosmos SignDoc), #6 (sound ZK Lasso verifier), #8 (PQC migration). This issue is solely the WASM policy REST surface + AppState wiring + UI re-enable; the wasm-policy engine itself already works.
Context
The
wasm-policycrate ships a working, sandboxedwasmtimepolicy engine (fuel + memory + epoch/time limits, module compilation + export validation, LRU module cache, optional on-disk persistence). However, none of it is reachable by a client: there are no/policiesREST routes, therest-apiAppStatehas noPolicyEngine, and the UI Policies page was deliberately replaced with aComingSoonplaceholder during the 12-PR quality + hardening pass.This was an intentional gating decision, not a regression. The gating landed in commit
63f8322("feat(rest-api): wire namespace + webhook management; gate hollow UI sections"), which wired up namespaces + webhooks but explicitly stubbed the Policies section rather than ship a half-wired path. This issue tracks completing that deferred work.The engine, store, and UI client/types already exist and are honestly labelled — the missing piece is the REST backend plus the
AppStatewiring and re-enabling the page.Current state (verified against live code)
Engine exists and is fully usable —
crates/wasm-policy/src/lib.rs:80-86re-exportsPolicyEngine,PolicyStore,Policy,PolicyMetadata,PolicyId,ResourceLimits, etc.PolicyEngine::with_store(...),register_policy,evaluate,evaluate_policy,simulate—crates/wasm-policy/src/engine.rs:44,90,95,120,424PolicyStoreCRUD:register,get,update,remove,list,list_metadata,enable,disable—crates/wasm-policy/src/store.rs:72,106,137,166,185,190,217,234MAX_POLICY_SIZE= 1 MiB,lib.rs:120), compiled viaModule::new, and exports checked viavalidate_exports—crates/wasm-policy/src/store.rs:76-88PolicyMetadata::new(...)+ builder (with_description,for_namespace, etc.) —crates/wasm-policy/src/policy.rs:164-216;Policy::new(metadata, bytecode)—policy.rs:292REST API does not expose any of it.
/policiesroutes registered.crates/rest-api/src/routes.rs:36-94nests only/keys,/audit,/auth,/namespaces,/webhooks.AppStatehas no policy engine field —crates/rest-api/src/middleware.rs:23-45(fields:sessions,key_manager,rate_limiter,rbac,namespaces,acls,webhooks,audit,start_time).hsm-rest-api'sCargo.tomldoes not depend onhsm-wasm-policy—crates/rest-api/Cargo.tomllistshsm-auth,hsm-audit,hsm-crypto-engine,hsm-key-manager,hsm-webhooksonly (lines 22-26).axumis pulled in withfeatures = ["json", "macros"]—crates/rest-api/Cargo.toml:11. Themultipartfeature is not enabled, so multipart WASM upload extraction is not currently available.AppStateviaAppState::with_key_manager(...).with_audit_logger(...)and callscreate_router—crates/hsm-server/src/main.rs:346-350. A livePolicyEnginemust be constructed here and threaded intoAppState.crates/rest-api/src/handlers.rs:1288("policy attachment is not yet wired").UI is built against routes that 404.
ui/app/(protected)/policies/page.tsx:1-11renders<ComingSoon ... detail="...uploading and managing policy modules over the REST API (multipart WASM upload + a live policy engine) is a future release." />.ui/lib/api.ts:256-295already implementslistPolicies(GET /policies),createPolicy(multipartPOST /policies,FormData),getPolicy,deletePolicy,attachPolicy(POST /policies/{id}/attach) — all currently hit a 404.PolicyandPolicyRequest(withwasm: File) —ui/lib/types.ts:225-242./api/hsm/:path*→${HSM_API_URL || http://localhost:8443}/:path*—ui/next.config.mjs:6-7, so route paths must match the client (/policies, not/api/policies).What is needed
hsm-wasm-policyas a dependency ofhsm-rest-apiand enable axum'smultipartfeature (crates/rest-api/Cargo.toml).PolicyEngineinAppState(crates/rest-api/src/middleware.rs): add apub policies: Arc<PolicyEngine>field, construct it innew/with_key_manager(aPolicyStore::with_storagefor persistence, or in-memory store, with sensible defaultResourceLimits). Wire construction incrates/hsm-server/src/main.rsnear line 346 so the deployed binary has it./policiesroutes to the authenticated router (crates/rest-api/src/routes.rs):GET /policies→ list (mapPolicyStore::list_metadata→ UIPolicyshape).POST /policies→ multipart upload (fields:name, optionaldescription,fuel_limit,memory_limit, and thewasmfile). BuildPolicyMetadata+Policy::new, callregister_policy(which validates size + compiles + checks exports). Return the createdPolicy.GET /policies/{id}→ get one.DELETE /policies/{id}→ remove.POST /policies/{id}/attach→ attach to a namespace (body{ namespace }); use the metadata namespace filter /PolicyStore::update.crates/rest-api/src/handlers.rs: enforce RBAC (policy management is privileged — gate behind the same permission tier as namespace/webhook management), surface compile/validation failures as400, mapPolicyError::NotFound→404. Note multipart bodies bypass the JSON path but are still bounded by the existingDefaultBodyLimit(1 MiB,routes.rs:20) — confirm this is compatible withMAX_POLICY_SIZE(also 1 MiB) or raise the limit for this route.Policyinterface (id,name,description?,wasm_hash,fuel_limit,memory_limit,created_at,attached_namespaces[]) —ui/lib/types.ts:225-234.wasm_hash←metadata.bytecode_hash,attached_namespaces←metadata.namespaces.ui/app/(protected)/policies/page.tsxComingSoonwith a real page that lists policies, uploads a.wasmmodule via the existinghsmApi.createPolicy(FormData), and supports delete + attach-to-namespace. The api.ts client is already written (ui/lib/api.ts:256-295) — no changes needed there beyond confirming paths.PolicyEngine::evaluatebefore performing a sign for a namespace with attached policies), or leave evaluation enforcement to a follow-up and scope this issue to management + storage only. State the chosen scope in the PR.Acceptance criteria
hsm-rest-apidepends onhsm-wasm-policy; axummultipartfeature enabled.AppStatecarries anArc<PolicyEngine>, constructed innew/with_key_managerand wired throughhsm-servermain.rs.GET /policies,POST /policies(multipart),GET /policies/{id},DELETE /policies/{id},POST /policies/{id}/attachare registered under the authenticated router and protected by RBAC..wasmmodule that exportsevaluatesucceeds and the policy appears inGET /policies; an invalid/non-compiling module returns400with a clear message; over-size returns413/400.Policytype;hsmApi.listPolicies/createPolicy/getPolicy/deletePolicy/attachPolicyall return non-404.ui/app/(protected)/policies/page.tsxno longer rendersComingSoon; it lists, uploads, deletes, and attaches policies against the live API.cargo clippy --all -- -D warnings,cargo test --all(skipping slow tests per CLAUDE.md), andcargo doc --no-deps --allare clean.Related (do not duplicate)
Distinct from #3 (threshold-ECDSA), #4 (SNIP-12), #5 (Cosmos SignDoc), #6 (sound ZK Lasso verifier), #8 (PQC migration). This issue is solely the WASM policy REST surface + AppState wiring + UI re-enable; the
wasm-policyengine itself already works.