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
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.
- 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.
- 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).
- 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.
- Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is
.optional()).
- 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
Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).
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 publishuploads artifacts whose bytes may no longer match their own declaredmanifest.integritydigests, 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
verifyIntegrity(files, integrity)(~40 lines + a structured rejection envelope) inpackages/core/src/security/besideplugin-artifact-signature.ts(which already declares itself byte-for-byte mirrored by cloud'spackage-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.os plugin publishpreflight atpackages/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.packages/spec/src/kernel/manifest.zod.ts:100-109and: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).packages/spec/liveness/manifest.jsonintegrity row (~:265-269) —statusstaysdead(its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so ifcheck:livenesssemantics 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..optional())..changeset/*.md):os plugin publishgains 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-
computeIntegrityartifact 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 inERROR_CODE_LEDGER— keep this card clear ofpackages/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)
packages/spec/src/kernel/manifest.zod.ts⇒ the PR parks as DRAFT withneeds:contract-reviewon both carriers (PR + card). Review chain owns enqueue.Blocked-by: (none)
Generated by Claude Code