Skip to content

POST /api/v1/packages is a publish-vs-install shape collision — REST registers first and shadows the dispatcher (#3587 finding) #3610

Description

@os-zhuang

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).

Refs: #3587, PR #3609; audit background docs/audits/2026-07-dispatcher-client-route-coverage.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions