Skip to content

Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

Description

@claude

Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

⚠️ Scope framing — this card does NOT discharge the #11331 ruling

The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

Task

  1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
  2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
  3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
  4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — status stays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
  5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
  6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

Decision mandate (pre-answered, carry in PR description)

Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

Error code

Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

Gates owed

pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

Clause ② (pre-recorded)

  • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
  • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

Blocked-by: (none)


Generated by Claude Code

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions