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
Found by the #3587 tranche-2 audit; ledgered as mismatch in packages/rest/src/rest-route-ledger.ts (PR #3609). This is the only route in the monorepo where the same verb+path carries two different semantics on two surfaces.
The collision
REST (packages/rest/src/package-routes.ts:49): POST /api/v1/packages = registry publish. Requires {manifest, metadata} in the body; 400 Missing required fields: manifest, metadata otherwise.
Dispatcher (packages/runtime/src/domains/packages.ts): POST /packages = install — different body, different semantics; client.packages.install targets this shape (dispatcher route ledger row POST /packages → packages.install).
Registration order decides: the production stack (packages/cli/src/commands/serve.ts:1748-1780) registers the REST plugin FIRST, the dispatcher second, and the underlying routers are first-match-wins with no specificity sorting (documented at rest-server.ts:1866-1877).
Consequence
In any stack where the package service resolves (REST's package routes are service-gated, rest-api-plugin.ts:300-321), client.packages.install's POST lands on the publish handler and should 400 — the SDK's install path is broken exactly in the fullest deployments. In stacks without service-package, the dispatcher serves install fine, which is why nothing obviously screams. (Not yet reproduced end-to-end — the audit is static; a boot-and-call repro should be the first step.)
The other three REST package routes (GET /packages, GET /packages/:id, DELETE /packages/:id) shadow their dispatcher twins compatibly (list additionally merges registry + database), so only POST is semantically wrong.
Options
A (recommended): consolidate on the dispatcher domain. Port what's unique in the REST handlers (registry+db merge on list; protocol.deletePackage full uninstall, Package uninstall does not clean up data-plane permission rows by package_id (ADR-0086 D3 wiring gap) #2747) into domains/packages.ts, then delete package-routes.ts and its plugin wiring. Kills a RouteManager-bypassing registrar too (the ledger's direct-mount source shrinks to external-datasource only). The dispatcher domain is the audited, 17-route, ledgered surface — one owner for /packages.
B: rename REST's publish to POST /packages/publish and keep both surfaces. Smaller, but leaves two owners for one prefix and the shadowing hazard for future routes.
Acceptance
One surface owns POST /api/v1/packages; a boot-level test proves client.packages.install works in a full stack (package service present).
Both route ledgers updated (mismatch row resolved; conformance suites green).
Found by the #3587 tranche-2 audit; ledgered as
mismatchinpackages/rest/src/rest-route-ledger.ts(PR #3609). This is the only route in the monorepo where the same verb+path carries two different semantics on two surfaces.The collision
packages/rest/src/package-routes.ts:49):POST /api/v1/packages= registry publish. Requires{manifest, metadata}in the body; 400Missing required fields: manifest, metadataotherwise.packages/runtime/src/domains/packages.ts):POST /packages= install — different body, different semantics;client.packages.installtargets this shape (dispatcher route ledger rowPOST /packages → packages.install).packages/cli/src/commands/serve.ts:1748-1780) registers the REST plugin FIRST, the dispatcher second, and the underlying routers are first-match-wins with no specificity sorting (documented atrest-server.ts:1866-1877).Consequence
In any stack where the
packageservice resolves (REST's package routes are service-gated,rest-api-plugin.ts:300-321),client.packages.install's POST lands on the publish handler and should 400 — the SDK's install path is broken exactly in the fullest deployments. In stacks withoutservice-package, the dispatcher serves install fine, which is why nothing obviously screams. (Not yet reproduced end-to-end — the audit is static; a boot-and-call repro should be the first step.)The other three REST package routes (
GET /packages,GET /packages/:id,DELETE /packages/:id) shadow their dispatcher twins compatibly (list additionally merges registry + database), so only POST is semantically wrong.Options
protocol.deletePackagefull uninstall, Package uninstall does not clean up data-plane permission rows by package_id (ADR-0086 D3 wiring gap) #2747) intodomains/packages.ts, then deletepackage-routes.tsand its plugin wiring. Kills a RouteManager-bypassing registrar too (the ledger'sdirect-mountsource shrinks to external-datasource only). The dispatcher domain is the audited, 17-route, ledgered surface — one owner for/packages.POST /packages/publishand keep both surfaces. Smaller, but leaves two owners for one prefix and the shadowing hazard for future routes.Acceptance
POST /api/v1/packages; a boot-level test provesclient.packages.installworks in a full stack (package service present).mismatchrow resolved; conformance suites green).Refs: #3587, PR #3609; audit background
docs/audits/2026-07-dispatcher-client-route-coverage.md.