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
{{ message }}
Repository navigation
[security] Stored-metadata-body family: two exits #21120 did not reach. An engine-only door serves an administrator credential material in cleartext, and the data door serves a content hash computed over the withheld credentials #21207
Triage: first grade — bug · security · priority:p1 · domain:cli · area:access · pm:queue. Exit one joins the family's projection and pin; exit two's hash becomes keyed, not narrowed
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T16:53Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only.
Why p1. Both exits were measured with an administrator's credential, and members are refused. It is the family's grade (#21120, p1). Exit one serves credential material in cleartext, so it is first in the PR.
Routing. Exit one lands in packages/mcp (domain:cli). Exit two lands in the metadata repository (domain:engine), declared as the same claim's cross-lane surface and dispatched vertically.
The ruling (triage's; overturnable by the maintainer):
Exit one. The engine-only reader joins the family's one projection: it serves the projected body, or refuses. It is added to the family's enumeration pin (stored-metadata-body-family.pin.test.ts), so a future reader fails the pin instead of joining silently. ⛔ No second projection.
Exit two: a keyed hash, not a narrowed one.
Hashing only the projected body would stop a credential-only change from moving the version, which breaks version identity.
So the served content hash becomes a keyed hash (HMAC under a server-held key). The version still moves on every change, and no offline verifier exists.
The claim lists every consumer that compares these hashes. If one compares across deployments, it says so here before building.
Measure in the same claim: the datasource body on exit one, which is affected by construction but not measured.
Pins: each exit, as an administrator, serves no credential material and no unkeyed hash, with a credential-free body as the control.
Claim: PM loop round 1 of the domain:cli seat's session session_01VvcEokUG1tvVxkceYfR5XB (batch 3): priority:p1security, triage's first grade 5936231568, filling a free slot under the maintainer's 「并发保持3」
Session: session_01VvcEokUG1tvVxkceYfR5XB
Account: huangyiirene
Branch: claude/issue-21207-stored-body-two-exits
Worktree: objectstack-issue-21207
Domain: domain:cli
Seat: domain:cli#1
File surface (classes and positions only, ⛔ no request, field path or value):
Exit one, the engine-only reader:packages/mcp/src/stdio-data-bridge.ts (its query and get paths, around :146 / :353 on 097ef80270) and packages/mcp/src/plugin.ts (the record resource, around :597).
It is added to the family's enumeration pin packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts, so a future reader fails the pin instead of joining silently.
Exit two, the served content hash:packages/metadata-protocol/src/sys-metadata-repository.ts, the row hash and the version it feeds, a cross-lane surface declared per triage. The served hash becomes keyed (HMAC under a server-held key), so the version still moves on every change and no offline verifier exists. ⛔ It is not narrowed to the projected body.
Stop clause: before building, the dev lists every consumer that compares these hashes (version history, parent links, the save receipt, drift or sync checks, anything else) and where the server-held key comes from. If any consumer compares hashes across deployments, or the key source needs a new secret, the dev stops and reports it here first.
Measure in the same claim: the datasource body on exit one, affected by construction but not yet measured.
Pins:
for each exit, an administrator gets no credential material and no unkeyed hash;
a credential-free body is the control;
a member stays refused.
One .changeset/21207-*.md at the level the real diff takes.
⚠️ Disclosure discipline: the PR body, the changeset and test titles name classes, doors, roles, codes and statuses only. ⛔ No request, header, field path, value or seeding recipe in public text.
(stop on a breach outside these; explain in the report)
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable).
Clause-②: no
Thread-read: 5936231568
Serial constraints cleared: No open PR touches packages/mcp/src/**, packages/metadata-protocol/src/sys-metadata-repository.ts, the family pin or protocol.ts (the file lists of all 16 open PRs were read in this act, main 097ef80270). In flight on this seat: #20752 stage 3 (packages/runtime strings), disjoint. area:access in flight elsewhere: #21185, #21180, #21154 and #21079, on drivers, forms spec, plugin-audit and plugin-security, all disjoint.
os-dev-report
{
"issue": 21207,
"status": "needs_decision",
"branch": "claude/issue-21207-stored-body-two-exits",
"pr": "#21228",
"session": "session_01VvcEokUG1tvVxkceYfR5XB — mode:subagent, the dispatching domain:cli seat session id",
"premise_still_valid": true,
"summary": "Exit one is delivered in draft PR #21228 (Part of #21207). The MCP stdio transport's engine-only reader (the stdio bridge's query, get and aggregate verbs, plus the record resource) now serves stored-metadata rows through the family's one projection (redactStoredMetadataRow, @objectstack/spec/kernel). It refuses the evaluate shapes (group, filter, sort, aggregate member on the stored body column) with the data door's INVALID_FIELD / 400. The family enumeration pin gains the two MCP rows; a new per-package pin classifies every bridge member and enumerates every engine read call site of packages/mcp/src on the syntax tree. Exit two was not built: it stopped at the claim's stop clause because the keyed hash needs a key source that does not exist. No existing keyed primitive reaches the metadata repository, so it needs either a new secret or a new keyed method on the crypto-provider contract. The decision is open_questions[0]; #21207 remains open for it. Premise measured live: the datasource body, affected by construction, is affected by measurement (cleartext to an administrator via the record resource and the bridge, member refused). The served content hash is an unkeyed sha256 over the whole stored body and equals the save receipt's version.",
"tests": "Head ae754f9. @objectstack/mcp pnpm test: Test Files 33 passed, Tests 366 passed (exit 0). @objectstack/mcp pnpm typecheck: exit 0, 'check:test-typecheck: OK' and the new test file is in the tsconfig.test.json program (listFilesOnly count 1). @objectstack/metadata-protocol vitest run: Test Files 200 passed / 3 skipped, Tests 2973 passed / 19 skipped (exit 0). @objectstack/metadata-protocol pnpm typecheck: exit 0, and the family pin is in its program. New pin src/stdio-data-bridge.stored-metadata-body.test.ts: 22 passed. Ablations: the fix was committed first, each mutation went through scripts/ablation-replace.mjs (anchor hit, blob changed), and each restore was proven 'blob == HEAD and git diff HEAD empty'. A1 (projection removed in serveStoredMetadataRow): 5 failed / 17 passed; red = the administrator no-credential pins (query, get, unjudgeable body, body-only projection, record resource), green = controls, refusals, member, teeth. A2 (evaluate refusal disabled): 9 failed / 13 passed; red = all 9 refusal pins, green includes the scalar-column control. A3 (an extra engine read behind a cast in plugin.ts): 1 failed / 21 passed. A4 (an unclassified bridge member): 1 failed / 21 passed. First attempts of A3 and A4 were no-ops: the replacement kept the anchor, so the tool refused and nothing ran; both were re-run with new anchors. A3 against the original name-based scan stayed green, so that scan was replaced by the AST scan and A3 was re-run red. No dist-reading committed test exists: the dogfood probe was private. The probe ran against a rebuilt mcp dist (marker count of the new function in dist: 3).",
"mcp_calls": "0 — no MCP GitHub tool was called; GitHub reads were single-card REST reads (issue #21207 and its comments, the #20790 dev report comment, PR #21228 read-back)",
"api_writes": "3 — all through scripts/pm/ tools over the fleet-write relay (each a POST /repos/objectstack-ai/objectstack/dispatches with the session token, executed by the relay as objectstack-fleet[bot]): (1) fleet-write/dispatch.mjs pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #21228, body read back identical 8720 bytes); (2) label-write.mjs --assign huangyiirene → POST /repos//issues/21228/assignees (read back: assignee matches); (3) post-stamped.mjs → POST /repos//issues/21207/comments (this report). git push is not REST.",
"open_questions": [
{
"question": "Exit two: the ruled keyed hash has no key source in the tree, and its at-risk population is the stored rows that already exist. Which key, and at which boundary? Measured: datasource save door refuses a credential in the body (422), and flows leave the body with #20790. So a hash keyed at WRITE time protects mainly flows written before #20790, while the existing stock and its history rows (the rows that actually carry credential material) keep serving the unkeyed sha256 unless they are re-keyed.",
"options": [
"A — write-time keyed hash under a NEW dedicated server secret (refuse production boot without it; dev/test a persisted local key), plus a one-off re-key migration of every existing row's checksum and previous_checksum. Cost: a new operator secret in every deployment; amending the repository contract's canonical-hashing invariant and the shared contract suite (metadata-core); the spec description of the history checksum; the stored-row checksum column bound (71 chars, a keyed prefix needs 76); the second producer in packages/metadata (DatabaseLoader) moved to the same function; rewriting append-only history; client-held versions answer one 409 after the switch.",
"B — serve-time keyed transform. Stored checksums stay canonical sha256 (contract, suite, both producers, FS layer and parent links untouched); every serve exit of a hash serves HMAC(key, stored hash), and every inbound version (If-Match / parentVersion, revert-to-version) is compared in that form. The key comes from the existing OS_SECRET_KEY through a new keyed method on the crypto-provider contract (spec contract widening, Clause-② yes; every provider implements it). No provider wired ⇒ the hash is withheld, never served raw. Cost: a hash-serving exit family to enumerate and pin (data door both tables, this MCP door, save receipt, /meta version and history, ETag), one contract method, and old clients' held versions answer one 409.",
"C — re-rule without a key: serve no hash for a row whose projection withheld material (the oracle exists only there), and keep serving the stored hash for every other row. Cost: those rows lose a served version, so their optimistic lock falls back to the existing last-write-wins path. Triage's 'keyed, not narrowed' ruling rejected hashing the projected body; this is a different narrowing and would need the ruling reopened."
],
"recommendation": "B, with C as the interim only if a contract widening is unwelcome now. 实际业务需求: the measured at-risk population is the existing stock and its history. B covers it on every exit without rewriting append-only history; A needs the re-key migration to reach it at all, and write-time keying alone protects almost nothing measured. 项目长远合理性: B keeps the version layer's canonical-hash contract intact and treats the keyed form as a serve-boundary concern, like the body projection, and the key stays with the existing secret owner (KMS-compatible). A changes the repository contract and runs two producers through a new secret. 防 AI 写错: B is one serve-time function plus an enumerated, pinned exit family, the pattern this body family already proved, and a missing provider fails closed. 创业阶段不扩散: B adds no operator secret and one contract method; A adds a secret, a migration and contract amendments; C adds nothing but reopens a ruling and degrades locking on exactly the credential-bearing rows."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — the generic data door's body-column filter refusal matches the exact column name, while this PR's door also refuses a dotted path into the body by its head segment. Whether the data door evaluates a dotted path into the stored body was read from source only and not measured, so it is not a finding. It is in PR #21228's Acceptance notes.",
"carrier: 承接者:无 · noted, not filed — packages/metadata's DatabaseLoader is a second producer of the stored-row and history checksum columns, with a different canonicalization (calculateChecksum) than the repository's hashSpec. Whether its save path is live in a production composition was not measured; it is listed in hash_consumers because exit two must move it too."
],
"gates": {
"head": "ae754f9e3a",
"derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands → 61 commands (exit 0; tree objectstack-ai/objectstack at ae754f9, change set 5 paths vs merge base bbcd20c)",
"ran": "61 of 61, each recorded 'COMMAND :: exit CODE', all exit 0 at ae754f9. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (8 packages without dist); it was re-run after building them and passed, exit 0. check:comment-mask-adoption first went red on a private comment stripper in the new test; fixed by moving the scan to the TypeScript AST, then exit 0.",
"reconcile": "dispatch-gates --ran: 61 derived, 61 run, 0 NOT-MEASURED (a DERIVED zero — all 61 recorded an exit code and none is 3)",
"lint": "pnpm lint (eslint . --no-inline-config, repo-wide, not narrowed): exit 0 at ae754f9, 139 s wall on a shared box",
"ci": "not awaited (in_progress at report time; CI convergence is the PM's)"
},
"deviations": [
"PR body first line is 'Part of #21207', not the dispatch's 'Fixes #21207': exit two is not built, so the merge must not close the card.",
"Clause-② is declared 'no (narrowing)', not the claim's 'no'. The diff narrows what a published door serves (projection instead of stored bytes) and accepts (evaluate shapes refused). This follows the family precedent (#21144 changeset): minor, BREAKING, ADR-0087 not-required (no-migration-prescription), and check:adr-0087-registration reads it '[BREAKING+bang+clause-②-narrowing]'.",
"Exit two stopped at the claim's stop clause (the key needs a new secret or a new contract method). No exit-two code was written, as the dispatch ordered.",
"Exit one's pins hold for credential material, the credential-free control and the member refusal. The 'no unkeyed hash' half is NOT MET: the stored checksum column is still served as stored and waits on open_questions[0].",
"The aggregate verb of the same stdio bridge is included in exit one. It is the same file and the same engine-only reader, and a group key on the body measured as serving the body.",
"The dispatch did not carry the four-axis decision framework. The recommendation uses the canonical copy in .claude/skills/pm-dispatch/SKILL.md 升级与决策, read once and not retained.",
"Measurement used a private probe test. It was copied into packages/qa/dogfood/test only for each run and removed right after (git status empty each time) and was never committed. Its readings are in the scratch folder issue-21207 (measure-before.json, measure-after.json).",
"The datasource credential case was measured on historical-stock rows, written straight to the two tables as the system, because the datasource save door refuses a credential in the body (measured 422).",
"Commits carry the model-free trailer pair from the dispatch and AGENTS.md. The harness attribution reminder asked for a model-named trailer, which was not used.",
"Labels documentation, size/l, tests and tooling on PR #21228 were applied by the repository's labelers, not by this run (the only label-write was the assignee)."
],
"files_changed": [
"packages/mcp/src/stdio-data-bridge.ts",
"packages/mcp/src/plugin.ts",
"packages/mcp/src/stdio-data-bridge.stored-metadata-body.test.ts",
"packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts",
".changeset/21207-mcp-stdio-stored-metadata-body.md"
],
"repro": {
"boot": "real boot (showcase + security + automation + SQLite file DB + connector plugins); administrator and member API keys minted through the key door and resolved the way the stdio door resolves them; before = origin/main 097ef80 build, after = branch build with rebuilt @objectstack/mcp",
"exit_one": [
{
"body": "flow body with credential",
"role": "administrator",
"before": "LEAK on record resource (resources/read), bridge get and query, both tables",
"after": "served as projection, no credential, on every path"
},
{
"body": "datasource body with credential (historical stock)",
"role": "administrator",
"before": "LEAK on record resource, bridge get and query, both tables",
"after": "served as projection, no credential, on every path"
},
{
"body": "credential-free body (flow and datasource)",
"role": "administrator",
"before": "served",
"after": "served byte-identical (same byte counts before and after)"
},
{
"body": "any",
"role": "member",
"before": "refused PERMISSION_DENIED on every path; the record resource answers an error text, no row",
"after": "same"
},
{
"body": "flow, datasource",
"role": "administrator",
"shape": "filter on the body column",
"before": "served — answered a guess (match vs no match)",
"after": "refused INVALID_FIELD / 400, engine not asked"
},
{
"body": "flow, datasource",
"role": "administrator",
"shape": "sort on the body; group by the body; aggregate filter on the body",
"before": "served (body in the answer / guess answered)",
"after": "refused INVALID_FIELD / 400"
},
{
"body": "flow, datasource",
"role": "administrator",
"shape": "aggregate member over the body",
"before": "refused by the engine INVALID_FIELD / 400",
"after": "refused here first INVALID_FIELD / 400"
},
{
"body": "flow, datasource",
"role": "member",
"shape": "evaluate shapes",
"before": "refused PERMISSION_DENIED",
"after": "refused INVALID_FIELD / 400 (before the engine, the data door's order)"
},
{
"body": "any",
"role": "administrator and member",
"shape": "tools query_records / get_record",
"before": "refused by the tool layer's system-object guard",
"after": "unchanged"
}
],
"exit_two": {
"served_hash_keyed": "NO — before and after: an unkeyed sha256 over the whole stored body (credential included), served on the generic data door and on this MCP door, equal to the save receipt's version (measured true)",
"version_moves_on_every_change": "YES today (the hash covers the whole body, credential included) — unchanged, nothing built",
"offline_verifier_defeated": "NO — not built (stopped at the stop clause)"
}
},
"hash_consumers": [
{
"where": "packages/metadata-protocol/src/sys-metadata-repository.ts:612-614 put()",
"compares": "caller-held parentVersion vs the stored checksum (optimistic lock)",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:620 put()",
"compares": "freshly computed hash of the incoming body vs the stored checksum (no-op short-circuit; needs a key stable across restarts and nodes)",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:485 getByHash() (ADR-0009 execution pinning), forwarded by metadata-core layered-repository.ts:96",
"compares": "a version looked up by history checksum; no production caller found in this checkout",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:785-828 delete()",
"compares": "parent lock on the stored checksum; tombstone records previous_checksum",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:922-997 promoteDraft() / dropPromotedDraftRow()",
"compares": "draft drained by its own checksum; parentVersion = active hash",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:1249 history read; :1926 rowToItem fallback",
"compares": "serves the parent chain (previous_checksum → parentHash); recomputes hashSpec(body) when a row has no checksum",
"cross_deployment": false
},
{
"where": "packages/metadata-protocol/src/protocol.ts:17673-17687 saveMetaItem, :18318 restore (parentVersion = history row checksum), :21898, :23031-23033 delete",
"compares": "parentVersion from the caller or the current hash vs the stored checksum; the save receipt echoes the version",
"cross_deployment": false
},
{
"where": "packages/rest/src/rest-server.ts:6992, :7222 (If-Match → parentVersion); packages/client/src/index.ts:806 (SDK If-Match); packages/cli/src/commands/meta/delete.ts (--if-match)",
"compares": "a client-held served version round-tripped to the same server",
"cross_deployment": false
},
{
"where": "packages/metadata/src/loaders/database-loader.ts:725-728, :1356-1366, :1404-1414 (calculateChecksum), :1002, :1145 (etag)",
"compares": "a SECOND producer of the same checksum / previous_checksum columns with a different canonicalization; no-op skip by equality; serves the checksum as an etag",
"cross_deployment": false
},
{
"where": "packages/metadata/src/metadata-manager.ts:3393",
"compares": "two versions' checksums side by side (version compare)",
"cross_deployment": false
},
{
"where": "packages/metadata-fs/src/repository.ts:261, :282, :361, :517, :530, :1004",
"compares": "the FS layer's own unkeyed hashSpec for file items; no FS-vs-DB hash comparison found (LayeredRepository probes each layer with its own hashes)",
"cross_deployment": false
},
{
"where": "packages/objectql/scripts/dry-run-hash-compat.ts",
"compares": "recomputes hashSpec(body) offline and compares with the stored checksum — an offline verifier by design",
"cross_deployment": "offline tool, same database"
},
{
"where": "packages/metadata-core/src/repository.ts:24 invariant 4; contract-suite.ts:207, :230, :270; packages/metadata-protocol/src/sys-metadata-repository.contract.test.ts",
"compares": "item.hash === hashSpec(item.body) — the repository contract every implementation (this one included) is tested against",
"cross_deployment": false
},
{
"where": "packages/spec/src/system/metadata-persistence.zod.ts:425; packages/metadata-core/src/objects/sys-metadata.object.ts:157-162 (maxLength 71); sys-metadata-history.object.ts:111-125 (maxLength 80)",
"compares": "declared shape: the history checksum is described as SHA-256 of the content; the stored-row column is bounded to a sha256-prefixed value",
"cross_deployment": false
},
{
"where": "cross-deployment consumers",
"compares": "none found in this checkout; the cloud repository (environment promotion, marketplace) was not available — NOT MEASURED",
"cross_deployment": "NOT MEASURED (cloud)"
}
],
"key_source": "None existing is reachable. The crypto provider contract (packages/spec/src/contracts/crypto-provider.ts) offers encrypt, decrypt, rotateKey and digest. Its digest may be plain SHA-256, and LocalCryptoProvider.digest is unkeyed sha256 (packages/services/service-settings/src/local-crypto-provider.ts:500). The provider is attached by the host after runtime.start (packages/cli/src/commands/serve.ts:5080-5092) into a private engine field (packages/objectql/src/engine.ts:3666), and the metadata repository cannot reach it. Its key material is OS_SECRET_KEY / OS_DEV_CRYPTO_KEY / a persisted file / an ephemeral test key, resolved inside service-settings. So a keyed hash needs a new secret, or a new keyed method on the crypto-provider contract plus a route from it to the hashing boundary. Either one triggers the claim's stop clause.",
"coexistence": "Write-time keying (option A): new rows store a keyed value and legacy rows keep sha256. The parent chain stays intact because previous_checksum copies the parent's stored value, so a keyed child points to an unkeyed parent. The first re-save of an unchanged legacy body mints one extra version unless no-op detection also accepts the legacy form. getByHash and history lookups keep matching stored values. Legacy rows keep serving their sha256, which is the offline verifier for exactly the stock at risk, until a re-key migration rewrites checksum and previous_checksum together. Serve-time keying (option B): nothing stored changes; legacy and new rows are served through the same transform, so the served chain stays consistent, and only versions clients held from before the switch answer one 409.",
"cleanup": "Worktree /home/user/objectstack-issue-21207 is clean (git status empty). After this comment posts: rm -rf its node_modules, then git worktree remove without --force. No server or monitor was started; the one gate batch that the harness moved to the background was waited on in the foreground with tail --pid and has exited."
}
packages/spec/src/contracts/crypto-provider.ts:155-161,digest 的说明是「SHOULD NOT reveal the plaintext (use HMAC or SHA-256 of canonical JSON)」。即契约允许不加密钥,所以不能依赖它来提供本安全性质。
协议声明:选项 B 要给加密提供者契约加一个方法,是公开契约放宽(Clause-②: yes),走 spec 车道并需契约复审;选项 A 要修改上面的不变量 4。
ACCEPT: PR #21228 at ae754f9e (exit one of #21207: the MCP stdio transport's engine-only reader joins the stored-metadata-body family). Part of: exit two waits on the maintainer (5938508053)
Review route: the seat reviewed this PR itself, with no separate contract-review agent.
No contract face is hit: the claim 5937050935 declares Clause-②: no, and the PR declares no (narrowing). A narrowing is not a Clause-② widening.
The diff touches no packages/spec/src/** and no governed path.
The rule is the contract-review rule as amended on main in 5e5ce48cef.
Exit one against triage's ruling 5936231568 item 1:
One projection: the bridge's query and get and the record resource serve through the family's one projection, redactStoredMetadataRow from @objectstack/spec/kernel. No second projection is written.
Engine first: the projection is applied to what the engine returns. A throwing redactor is not caught, so the read fails closed.
Evaluate shapes: a grouping, filter, sort or aggregate member on the body column is refused INVALID_FIELD / 400 after the exposure gate and before the engine. This covers the call's filter and each aggregation's own filter. A path whose head is the body column counts as the column.
Body-only projection: a projection naming the body without the type column reads the type and drops it again, so the caller gets exactly the columns it named.
Write verbs: the stored-body tables declare no write verb, so the exposure gate refuses create / update / remove there. This is pinned.
The family pin:stored-metadata-body-family.pin.test.ts gains the seam and refusal rows for @objectstack/mcp (6 → 8).
The package's own pin: it classifies every bridge member and enumerates every engine read call site on the syntax tree, so a new reader fails there.
Measured (the os-dev-report5938425782, real boot, before vs after):
an administrator's reads of a credential-bearing flow body and datasource body: served in cleartext before, served as the projection after, on all three paths and both tables;
credential-free bodies: byte-identical;
members: refused as before.
The datasource body, which triage asked to measure, was affected and is now covered.
Ablations: the dev ablated four legs on committed source, each red, then restored with blob == HEAD:
"What stays answerable".
All hold. "No export or published type changes" holds: the new helpers are exported from stdio-data-bridge.ts, but src/index.ts does not re-export that module, and the package publishes only .. The adr-0087: not-required marker reads as the gate expects.
Disclosure: the PR body, the changeset and the test titles name classes, doors, roles, codes and statuses only.
Seat verification: checks on ae754f9e: 34 names, 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 red; Lint & Repo Gates and TypeScript Type Check are success. git merge-tree against origin/main: clean against 62b90d74f7, and no main commit since the merge base touches packages/mcp, the family pin or @objectstack/spec/kernel. check-governed-merges --pr 21228: NOT governed, +805 / −9.
Checklist:
Draft, base main, first line Part of #21207.
5 files: two source files, two pins and one changeset, all within the claim's exit-one surface.
Not met here, by design: the "no unkeyed hash" half of the claim's pins is exit two. It stopped at the claim's stop clause because a keyed hash needs a new secret or a new contract method. The decision is in the maintainer's inbox as 5938508053.
Out-of-scope findings, one line each:
The generic data door's refusal matches the body column's exact name, while this door also refuses a path into it. Whether the data door evaluates such a path was read from source, not measured. Acceptance notes.
A second producer of the stored-row checksum (packages/metadata's database loader) uses a different canonicalization. Whether it is live was not measured. Acceptance notes; it is input to exit two's execution.
Next: readied and armed in this act. At the merge the seat records exit one, and in the same act moves this card from pm:dispatched to needs-user-decision, as 5938508053 states.
Landed: exit one, PR #21228 → 3ddd3d0c4a (the MCP stdio transport's engine-only reader serves the stored body only as its type's read projection and refuses to evaluate it). A Part of landing: exit two waits on the maintainer
Landing reading:3ddd3d0c4a is on origin/main as a single-parent squash on 62b90d74f7, 5 files, +805 / −9. Its diff equals the PR's net diff at head ae754f9e line for line, once the index and hunk-header lines are set aside.
What is closed: exit one of this card. An administrator's key on the MCP stdio transport no longer reads stored credential material from either stored-body table, and the evaluate shapes on the body column are refused. The family pin now carries 8 surfaces.
What remains: exit two, the served content hash. It stopped at the claim's stop clause, and the decision is 5938508053: A, B or C, with the seat recommending B.
State:pm:dispatched → needs-user-decision, and the assignee is cleared, in the same act as this note. The card leaves the active queue until the maintainer answers. The seat's execution resumes from the ruling's "裁后执行" section.
os-dev-report
{
"issue": 21207,
"status": "done",
"branch": "claude/issue-21207-exit-two-keyed-served-hash",
"pr": "#21436",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Exit two of #21207 under ruling B and forks A/A/A, one draft PR for the whole family. Every door that served a metadata body's stored content hash now serves the crypto provider's keyed digest of it (nothing with no provider); every inbound version token is compared in keyed form against the current head and the stored value is handed to the repository lock (raw or stale: 409 METADATA_CONFLICT; no provider: every token refused 409). Filter, sort and group on the two stored content-hash columns are refused 400 INVALID_FIELD before the engine at the data door, the MCP stdio reader and the analytics door; write-time copies (ledger snapshot and diff, activity copy, decision-audit note) carry no hash, and the extended os migrate audit-metadata-bodies (dry run default, idempotent) rewrites copies at rest while the version history stays the lineage. The after-measurement found member 14 (the version history change note quoting the stored hash on a message-less draft promotion), closed in the same PR and flagged for the contract review.",
"tests": [
"Red first (pins committed on the unfixed tree 8c610b8): metadata-protocol 25 failed / 5 passed; mcp 11 failed / 3 passed; plugin-audit 14 failed / 88 passed; service-analytics 6 failed / 7 passed. Every red a door serving or accepting the stored value; controls green. Member-14 pins and the decision-note copy pin were written after the fix; their red is shown by ablation legs L06, L10, L15, L17.",
"Green at the fix (all through os-verify-lock, VERDICT command-exit 0): metadata-protocol full suite 3013 passed (re-run on touched files after the main merge); objectql 7358 passed; rest 4982; runtime 5081; mcp 380; plugin-audit 598; service-analytics 3793; cli unit project 3489 passed; cli migrate preview integration file 6 passed 1 skipped (live PG cell); dogfood 17 affected files passed.",
"typecheck exit 0: metadata-protocol, objectql, mcp, plugin-audit, service-analytics, rest, cli.",
"Ablation: 17 legs via scripts/ablation-replace.mjs on the committed fix, anchor hit once, blob changed, targeted pin red, restore proven blob == HEAD blob and git diff HEAD empty (see ablations).",
"Gates at 1ad5a00: dispatch-gates --repo objectstack-ai/objectstack --commands derived 84, all 84 run, all exit 0; --ran reconciled 84 derived / 84 run / 0 NOT-MEASURED / 0 UNRUN; check:error-code-casing 0; check:nul-bytes 0; pnpm lint (whole repo) exit 0."
],
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "3 — all through the fleet-write relay: POST /repos/objectstack-ai/objectstack/pulls (draft pr_create, #21436, read back 12992 bytes sent / 12992 stored, identical); POST /repos//issues/21436/assignees (label-write --assign huangyiirene, read back matches); POST /repos//issues/21207/comments (this os-dev-report). git push is not a REST write. Reads: one REST GET of check-runs on the head.",
"open_questions": [
{
"question": "No crypto provider registered: the receipts' required version token is the empty string (it names nothing and every inbound comparison refuses it). The response schemas declare the token a required string, and spec is outside the claimed surface.",
"options": [
"A keep the empty token (this PR)",
"B spec follow-up making the receipt version optional so a provider-less deployment omits it"
],
"recommendation": "A in this PR, B as a spec-seat follow-up if the contract review prefers absence to an empty sentinel; A is already fail-closed on every inbound door."
},
{
"question": "Member 14 (history change note quoting the stored hash) is outside the ruling's literal enumeration of members 1 to 13; it is closed here (hash-free publish message, keyed serve of pre-existing notes, evaluate refusal at three doors, copies withheld, migration).",
"options": [
"A accept as a family member in this PR",
"B split into its own card"
],
"recommendation": "A, same oracle class on the same row, same doors and the same seams; splitting would ship a known hash exit beside a fix that claims the family."
},
{
"question": "Data-door search over the two stored-metadata tables also stops scanning the stored body column (a released #21120-class search oracle on the same row, touched by this narrowing).",
"options": [
"A keep in this PR (declared)",
"B revert the body-column half and file it separately"
],
"recommendation": "A, the narrowing is one list over the unsearchable columns and leaving the body in it keeps a prefix oracle on withheld material."
}
],
"out_of_scope_findings": [
"carrier: spec seat, at the contract review of #21436 · the spec descriptions of the save, publish and batch-publish tokens still say the token is currently emitted as an unkeyed hash; the format is declared outside the contract, prose drift only · noted in Acceptance notes, not filed",
"carrier: spec / kernel seat · the hash-column list's natural home is beside the body-column primitives in the spec kernel module; metadata-protocol, mcp, plugin-audit and service-analytics each hold a copy, metadata-protocol's pinned equal to the object definitions, the others pinned by behaviour tests · noted in Acceptance notes, not filed",
"carrier: 承接者:无 · realtime record events would carry the two hash columns; out of scope by the ruling, no public channel route in this repository · noted in PR body, not filed",
"carrier: 承接者:无 · metadata-core base conflict text prints both stored values; no door serves it (every door converts the conflict, the revert door withholds undeclared failures) · noted in Acceptance notes, not filed"
],
"exit_family": [
"1 save receipt token · packages/metadata-protocol/src/protocol.ts:18148 saveMetaItem via receiptVersion (packages/metadata-protocol/src/protocol.ts:15817) · keyed; empty with no provider",
"2 publish receipt token · packages/metadata-protocol/src/protocol.ts:19087 publishMetaItem · keyed",
"3 package batch-publish tokens · packages/metadata-protocol/src/protocol.ts:20635 publishPackageDrafts · keyed per element, stored value stays internal",
"4 rollback receipt token · packages/metadata-protocol/src/protocol.ts:22913 rollbackMetaItem · keyed",
"5 history event hash and parent hash · packages/metadata-protocol/src/protocol.ts:18806-18812 historyMetaItem · keyed per event; null with no provider",
"6 conflict refusal text and attributes · packages/metadata-protocol/src/protocol.ts:15856 metadataConflictRefusal, used at 18226 (save), 19365 (publish promote), 22921 (rollback), 23546 (reset/delete) · keyed values or (withheld) / null",
"7 decision-audit note of a conflict · packages/metadata-protocol/src/protocol.ts:15917 optimisticConflictAuditEntry · names no hash, each side (withheld) or null",
"8 the two hash columns on both stored-metadata tables · packages/metadata-protocol/src/protocol.ts:12008 findData, 12142 getData via packages/metadata-protocol/src/metadata-redaction.ts:872 and 894 · keyed; omitted with no provider",
"9 evaluate shapes on those columns · packages/metadata-protocol/src/protocol.ts:11901 findData via packages/metadata-protocol/src/metadata-redaction.ts:927; search narrowing packages/metadata-protocol/src/protocol.ts:11195 / 11765 via packages/metadata-protocol/src/metadata-redaction.ts:983 and 994 · 400 INVALID_FIELD naming usable columns",
"10 MCP stdio engine-only reader · packages/mcp/src/stdio-data-bridge.ts:492 refusal, :550 serve, wired :691-720 and :802; packages/mcp/src/plugin.ts:577 and :629 (record resource) · keyed; group, filter, sort refused INVALID_FIELD",
"11 audit ledger copies · packages/plugins/plugin-audit/src/audit-writers.ts:1439; migration packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts:114 and :288 · dropped at write time; at rest via migration",
"12 activity copies · audit-writers.ts:1439; stored-metadata-body-migration.ts:307 · same as 11",
"13 analytics members on those columns · packages/services/service-analytics/src/stored-metadata-body-refusal.ts:86 · 400 INVALID_FIELD in either role",
"14 history change note · packages/metadata-protocol/src/protocol.ts:19353 promoteDraftForPublish states a hash-free message; served keyed by packages/metadata-protocol/src/metadata-redaction.ts:835 (history packages/metadata-protocol/src/protocol.ts:18812, data door, MCP); evaluate refused at the three doors; copies audit-writers.ts:1441 and :1448; migration stored-metadata-body-migration.ts:134 and :186 · keyed or (withheld); history row not rewritten",
"engine accessor · packages/objectql/src/engine.ts:8578 getKeyedDigest beside setCryptoProvider · additive, read at each use, hands out the digest only",
"family pin · packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts · 23 surface rows with dispositions seam / keyed / withheld / refuses, plus a tooth holding the hash-column list equal to the object definitions"
],
"inbound_paths": [
"save door (/meta save and the runtime dispatcher save): packages/metadata-protocol/src/protocol.ts:18012 saveMetaItem reads the current head and resolves the sent token through storedParentForToken (packages/metadata-protocol/src/protocol.ts:15831); a keyed match hands the stored value to the repository lock; a raw stored hash or stale token: 409 METADATA_CONFLICT; no provider: 409 METADATA_CONFLICT (unverifiable)",
"reset door: packages/metadata-protocol/src/protocol.ts:23444 deleteMetaItem, same comparison and same refusals",
"in-process only: migrateStoredMetadata passes the stored parent through an internal request member (packages/metadata-protocol/src/protocol.ts:17988, :18683); the REST and dispatcher doors build their save request field by field and never spread the body, so no door reaches it",
"publish and rollback doors take no caller token; their race conflicts go through metadataConflictRefusal"
],
"evaluate_refusals": [
"data door: filter, sort, group on either hash column or the change note: 400 INVALID_FIELD before the engine; explicit search fields naming one: 400 INVALID_FIELD; auto-default search no longer scans them or the body column",
"MCP stdio reader: query where / orderBy and aggregate groupBy on the same columns: INVALID_FIELD",
"analytics door: dimension or measure on the same columns in either role: 400 INVALID_FIELD",
"member role before and after: data door 403 PERMISSION_DENIED, history 403, ledger 403, analytics 403 PERMISSION_DENIED, MCP PERMISSION_DENIED"
],
"copies": {
"write_time": "after the change, ledger and activity copies and decision-audit notes written on a real boot carry no hash (0 rows)",
"migration_cli_on_a_base_written_db": "before: ledger 25 of 38 with a hash, activity 25 of 38, decision notes 1; dry run exit 0: unchanged, reports 53 rows to rewrite; --apply --yes exit 0: ledger 0 of 39, activity 0 of 39, notes 0; second dry run exit 0: 0 to rewrite; history lineage keeps its 9 stored hashes",
"stock_db_unmigrated": "served copies still carry the hash until the operator runs the migration (the ruled path)"
},
"repro": {
"composition": "showcase + automation + SQLite file database + audit plugin + three connector plugins; admin and member API keys minted through the key door (201 / 201); local crypto provider registered as os serve does",
"before_base_ecb6ca0258": "receipts equal the stored head; history serves stored hashes; raw stored hash sent back accepted 200 on save and reset doors; 409 carries the stored hash; data door serves stored values and a right guess confirms; filter right 1 row / wrong 0; group serves stored; search matches a hash prefix and a credential prefix; ledger, activity, notes, analytics and MCP serve stored",
"after_this_branch": "all served tokens and columns keyed, stable across reads, moved by a credential-only change; raw inbound 409 METADATA_CONFLICT; served token sent back 200; evaluate shapes 400 INVALID_FIELD; search narrowed; copies hash-free at write time; MCP keyed and evaluate refused"
},
"gates": "at 1ad5a00: 84 derived by dispatch-gates --repo objectstack-ai/objectstack --commands, 84 run, every exit 0; --ran: 84 / 84 / 0 NOT-MEASURED / 0 UNRUN; pnpm lint whole repo exit 0 (no narrowing needed); hygiene: engine-double-contract --write ledger, objectql-double-limit (find double holds the bound), where-matcher (combinators refused), dispatcher-error-vocabulary (operator-form read), error-code-casing (persisted vocabulary marked), dual-build-cjs-loads (missing builds made)",
"deviations": [
"member 14 (history change note) added beyond the literal 1 to 13 enumeration",
"data-door search narrowing also removes the stored body column (released #21120-class hole touched by this narrowing)",
"test and ledger edits outside the literal surface: objectql batch-publish conformance pin (now registers a provider), two decision-note pins in metadata-protocol (now assert withheld), scripts/engine-double-contract.pinned.json via --write",
"the migration also rewrites decision-audit notes (same copy class, fork three A)",
"rest-server.ts: one comment only; runtime meta.ts and metadata-core errors.ts untouched",
"no-provider receipts carry the empty token (open question 1)",
"member-14 and decision-note copy pins shown red by ablation, not red-first",
"origin/main merged once into the branch (41a3c8d, includes #21377; no hash exit added)"
],
"files_changed": "22 files, +2663 / -137 vs 41a3c8d: .changeset/21207-keyed-served-content-hash.md; packages/cli/src/commands/migrate/audit-metadata-bodies.ts; packages/mcp/src/plugin.ts; packages/mcp/src/stdio-data-bridge.ts; packages/mcp/src/stdio-data-bridge.stored-content-hash.test.ts; packages/metadata-protocol/src/metadata-redaction.ts; packages/metadata-protocol/src/protocol.ts; packages/metadata-protocol/src/protocol.served-content-hash.test.ts; packages/metadata-protocol/src/protocol.data-door-stored-content-hash.test.ts; packages/metadata-protocol/src/protocol.lifecycle-audit-rows.test.ts; packages/metadata-protocol/src/protocol.package-publish-audit-rows.test.ts; packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts; packages/objectql/src/engine.ts; packages/objectql/src/publish-package-drafts-response-conformance.test.ts; packages/plugins/plugin-audit/src/audit-writers.ts; packages/plugins/plugin-audit/src/audit-writers.test.ts; packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts; packages/plugins/plugin-audit/src/stored-metadata-hash-migration.test.ts; packages/rest/src/rest-server.ts; packages/services/service-analytics/src/stored-metadata-body-refusal.ts; packages/services/service-analytics/src/stored-metadata-body-refusal.test.ts; scripts/engine-double-contract.pinned.json",
"ablations": [
"L01 receipt served raw: 8 of 11 red",
"L02 raw token accepted inbound: 2 of 11",
"L03 history served raw: 3 of 11",
"L04 conflict carries the stored hash: 2 of 11",
"L05 note carries the token: 1 of 11",
"L06 publish door states no message: 1 of 11",
"L07 data-door columns served raw: 5 of 27",
"L08 data-door evaluate shapes unrefused: 12 of 27",
"L09 search not narrowed: 5 of 27",
"L10 quoted hash in a note served raw: 2 of 38",
"L11 MCP columns served raw: 3 of 16",
"L12 MCP evaluate shapes unrefused: 8 of 16",
"L13 analytics unrefused: 7 of 14",
"L14 writer copies the hash: 4 of 93",
"L15 writer copies a decision note hash: 1 of 93",
"L16 migration keeps the columns: 7 of 12",
"L17 migration keeps a note hashes: 5 of 12",
"every leg restored: blob == HEAD blob, git diff HEAD empty"
],
"checks_after_push": "read once at head 1ad5a00, not polled: 31 check runs, 11 success (among them Check Changeset, Check PR Size, Governed Surface Queue Guard, the claim and single-writer guards), 3 skipped, 17 in_progress (Build Core, Lint and Repo Gates, Test Core 1-6, Type Check x4, Dogfood x4, Temporal Conformance); no completed failure. CI convergence is the PM's."
}
os-dev-report
{
"issue": 21207,
"status": "done",
"round": "patch round 1 (CI red on 1ad5a00: client and CLI reset-door pins)",
"branch": "claude/issue-21207-exit-two-keyed-served-hash",
"pr": "#21436",
"head": "7660d811a7",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "The seat's reading was confirmed by measurement. With no crypto provider, the first cut served an empty version token. The SDK and the CLI both drop an empty pin, by design and pinned, so a pinned reset went out unpinned and the lock failed open. Fix (one commit, 7660d81): while no provider is registered, the protocol keys tokens under a process-scoped ephemeral key (32 random bytes drawn on first use, held only in module memory). When a provider is registered, its key is read per use. Tokens are always served, differ per content, are never the stored hash, and an empty or withheld token sent back is refused with 409. The no-provider refusal branch is removed. The batch-publish conformance pin, which the first cut had re-composed, is restored to its base bytes. Both reset-door pins pass unedited.",
"premise_reading": [
"Ruling premise: no stored value persists a SERVED (keyed) token. Verified, it holds.",
"Save door: protocol.ts saveMetaItem resolves the sent token to the STORED head (storedParentForToken) before the repository is asked. The repository persists only stored hashes, as current and parent.",
"Reset door: deleteMetaItem does the same, and revertCommit passes the stored head internally.",
"Version history rows store the stored hash and stored parent. The change note is now hash-free; old notes are served keyed on read, not rewritten.",
"Decision-audit rows: the note is (withheld) or null, never a token (optimisticConflictAuditEntry).",
"Ledger and activity copies drop the hash columns at write time.",
"Package publish: the per-element token is computed only into the response (receiptVersion at published.push). The commit record holds items and seqs, no token. The audit note names package and commit only.",
"Rollback takes an integer history version, not a token.",
"The internal migration passes the stored parent in-process (storedParentVersion), never a served token.",
"CLI: os meta delete takes the pin from the operator per call and stores nothing. SDK: tokens are held in memory only; no browser storage in packages/client/src.",
"MCP: no metadata write door takes a version token.",
"A served token is therefore only ever held by a caller, and losing the key costs at most one 409 on a held token. Sister-repo clients (console, cloud) are not in this tree: NOT MEASURED, reason: not checked out here; for them the ruled cost is one 409."
],
"tests": [
"Prerequisite builds (os-verify-lock, VERDICT command-exit 0): client dependency closure, 34 packages; the 26 CLI dependencies that lacked a dist; @objectstack/cli itself.",
"Pins on the fix, as written: client src/meta-delete-item-carriers.test.ts 20 passed of 20; cli --project integration src/commands/meta/delete-reset-carriers.test.ts 20 passed of 20. Both are byte-identical to base 41a3c8d: client blob 7950f3f1d6d3, cli blob b6f9c518f0ff.",
"Full suites at 7660d81, each through os-verify-lock with VERDICT exit 0. Commands are pnpm --filter PKG exec vitest run --maxWorkers=2 unless noted:",
"metadata-protocol: 3031 passed, 19 skipped",
"client: 644 passed of 644",
"cli --project integration (queue population): 638 passed, 2 skipped",
"cli --project unit: 246 files, 3523 passed. Two files failed only because the cli dist was not built (prerequisite, not a red). After pnpm --filter @objectstack/cli build they were re-run: test/published-subpath-console.pin.test.ts and test/published-subpath-hook-body.pin.test.ts, 29 passed",
"rest: 4985 passed, 323 skipped",
"mcp: 382 passed",
"plugin-audit: 599 passed",
"service-analytics: 3802 passed, 89 skipped",
"objectql: 7365 passed, including the restored conformance pin, 15 of 15",
"plugin-hono-server, which sends the version header: 324 passed",
"metadata, the three repository-level files that send a version: 9 passed",
"cli nightly tier, OS_TEST_TIERS=nightly --project integration test/migrate-meta.e2e.test.ts (the internal stored-parent path): 19 passed",
"No other nightly-tier CLI file saves or reads a metadata version token (grep over the 78 e2e/live files: zero hits).",
"Targeted first, then: metadata-protocol protocol.served-content-hash, protocol.data-door-stored-content-hash and stored-metadata-body-family pins, 50 passed.",
"typecheck: metadata-protocol tsc --noEmit exit 0. No other package's source changed this round."
],
"ablations": [
"A1, source, metadata-protocol aliased to src. storedHashDigest falls back to an empty-token digest. Client real-door pin: 3 of 20 red, the exact CI failures (expected '' not to be '' twice; 'expected the stale scoped reset to be refused'). Protocol pins: 5 of 41 red. Restored: blob == HEAD, git diff HEAD empty.",
"A2, first attempt: a no-op. ablation-replace refused it because the replacement contained the anchor (anchor count 1 to 1). It restored automatically and the reading is void.",
"A2b, source. storedParentForToken reads an empty or withheld token as no pin. Served-content-hash pin: 2 of 14 red. Restored: blob == HEAD.",
"A3, dist. The empty-token mutation was built into the metadata-protocol dist; preflight found the marker in 2 built files. Base conformance pin in objectql: 1 of 15 red (expected 0 to be greater than 0). Restore leg: rebuilt, marker absent from all 24 files, tree clean, 15 of 15 green.",
"A4, dist. Same mutation, CLI integration pin: 6 of 20 red, the exact CI six. Restore leg: rebuilt, marker absent, tree clean, 20 of 20 green."
],
"gates": "At 7660d81. dispatch-gates --repo objectstack-ai/objectstack --commands derived 84 commands. The run before the container restart was killed; its partial record (56 lines) was set aside unread. The run after the restart recorded 82 commands at exit 0 and two at exit 3 (PREREQUISITE NOT MET): check:dual-build-cjs-loads, for missing dists, and check:i18n-coverage, because one example config could not load an unbuilt connector. Both were re-run after the dists existed, exit 0. --ran: 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. check:error-code-casing exit 0; check:nul-bytes exit 0 (in the derived set). pnpm lint on the whole repo: exit 0, 135s on a shared box.",
"dogfood": "NOT MEASURED this round. Reason: dogfood boots with a crypto provider registered, so the changed branch (the no-provider fallback) is off its path. It ran green in round one; CI runs it.",
"mcp_calls": "0",
"api_writes": "1: POST /repos//issues/21207/comments (this os-dev-report) through the fleet-write relay. git push 1ad5a00..7660d81 is not a REST write. Reads: REST GET of the check-runs on 7660d81, once.",
"checks_after_push": "Read once at 7660d81, right after the push: 0 check runs registered yet. Not polled; CI convergence is the seat's.",
"open_questions": [
{
"question": "Option A (an empty token with no provider) was falsified by CI and replaced by the ephemeral key, as ruled. One residual: on a multi-process deployment with no crypto provider, each process has its own key. A token served by one process is refused with 409 by another, so a client pinning against a load-balanced pool gets spurious 409s. os serve and the verify harness always register a provider, so this applies only to hosts that register none.",
"options": [
"A accept, and document "register a crypto provider for multi-process"",
"B a follow-up that derives the fallback key from a configured secret when one exists"
],
"recommendation": "A for this PR: the ruled cost is a loud 409, never a silent lost update. B only if a provider-less multi-process deployment is real."
},
{
"question": "Placement of the key: the ruling said "per engine". It is placed per process, in metadata-protocol beside the served-hash helpers, and read through the protocol's storedHashDigest. engine.ts is unchanged this round. Protocols in one process (per-environment) therefore answer one another's tokens, and a host engine without the accessor is covered too.",
"options": [
"A keep per process (this PR)",
"B move it into the engine accessor, so the MCP stdio reader also serves keyed columns with no provider instead of omitting them"
],
"recommendation": "A: it is the narrower change and it fails closed. With no provider, MCP omission is the safe direction, and MCP has no version-token door."
},
{
"question": "Carried from round one for the contract review: member 14 (change note), and the body-column search narrowing.",
"options": [
"A keep both in this PR",
"B split either"
],
"recommendation": "A, as argued in the round-one report."
}
],
"deviations": [
"PR body NOT edited. The coordinator asked for the open-question section to be updated, but the os-dev contract says the dev writes the PR body once, at creation, never PATCHes it, and names post-hoc edits for the seat to write; the contract wins on conflict. That conflict is stated here, not silently resolved. The exact replacement text is in pr_body_edits_for_seat. The first two lines are untouched.",
"The changeset was updated: consequence 1 now describes the ephemeral key, the one 409 after a restart or a provider registration, and the refusal of empty or withheld tokens. "What else changes" now says the MCP reader omits the columns with no provider. The ADR-0087 marker text was adjusted.",
"The ephemeral key is process-scoped in metadata-protocol, not per engine (open question 2). engine.ts unchanged.",
"Pre-existing pin restored: objectql publish-package-drafts-response-conformance.test.ts is back to base bytes (63f687725a1d). The first cut had added a provider to its composition, the pattern this round prohibits.",
"My own round-one pins changed (new in this PR, not pre-existing): the no-provider cases in protocol.served-content-hash.test.ts and protocol.data-door-stored-content-hash.test.ts now assert the keyed, non-empty, per-content, process-scoped behaviour and that a provider registered later moves tokens. A new case pins that an empty or withheld token is refused with a provider registered. One header line in the family pin was updated.",
"Container restart at about 17:52Z killed the first gate run. Its partial record was treated as unread and every gate was re-run. Suites and ablations had all completed, and been recorded, before the restart.",
"origin/main is at least 15 commits ahead (dispatch-gates note). No merge was made this round."
],
"files_changed_this_round": [
".changeset/21207-keyed-served-content-hash.md",
"packages/metadata-protocol/src/metadata-redaction.ts (ephemeralStoredHashDigest; doc text)",
"packages/metadata-protocol/src/protocol.ts (storedHashDigest never undefined; receiptVersion; storedParentForToken refuses empty; unverifiable branch removed; comments)",
"packages/metadata-protocol/src/protocol.served-content-hash.test.ts",
"packages/metadata-protocol/src/protocol.data-door-stored-content-hash.test.ts",
"packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts (header text)",
"packages/objectql/src/publish-package-drafts-response-conformance.test.ts (restored to base)"
],
"pr_body_edits_for_seat": "R1. In "Acceptance notes", replace the bullet that starts "No crypto provider registered." with:\n\n- No crypto provider registered (option A falsified by CI, replaced). The first cut served an empty version token on a host with no crypto provider. CI falsified that: two real reset-door pins, one in the SDK and one in the CLI, showed that every save then handed out the same empty token. A client that sends no pin for an empty token turned a pinned reset into an unpinned one, so the optimistic lock failed open. Replaced in this PR: while no provider is registered, the doors key under a process-scoped ephemeral key (32 random bytes drawn on first use, never written, logged or served). A token is always served, differs when the content differs, and is never the stored hash. An empty or withheld token sent back is refused with 409 METADATA_CONFLICT, never read as "no pin". A token held across a restart, or across a provider's first registration, is refused once with the same 409. No stored value carries a served token, so nothing persisted dies with the key. The MCP stdio reader has no version-token door; it still omits the hash columns on a host with no provider.\n\nR2. In "Superseded pins updated", replace the third bullet ("One batch-publish conformance pin asserted a non-empty token with no provider registered. It now registers a provider and asserts the keyed form.") with:\n\n- The batch-publish conformance pin asserts a non-empty token with no provider registered. The first cut changed its composition. It is back to its base bytes and passes as written.\n\nR3. In the exit-family table, the Disposition cells of rows 5 and 8 read "keyed per event; null with no provider" and "keyed; omitted with no provider". Replace them with "keyed per event" and "keyed".\n\nR4. Append after the "Ablations" table:\n\nPatch round (CI falsified option A). The fix lands at 7660d811a7. Validation and ablation results are in the os-dev-report for this round. The SDK and CLI reset-door pins pass unedited. Restoring the empty token, with dist rebuilt, turns them red again: 3 of 20 and 6 of 20, the exact CI failures.\n",
"out_of_scope_findings": [
"carrier: 承接者:无 · the first cut skipped @objectstack/client and the CLI integration tier because neither package's source changed, although both drive the real protocol and the served token's wire value changed. The import-er rule (the public face changed, so its test is owed) applied and was missed. Noted, not filed."
]
}
os-dev-report
{
"issue": 21207,
"status": "done",
"round": "patch round 2 (CI red on the merge of 7660d81 with main: the #21432 absent-database audit pin)",
"branch": "claude/issue-21207-exit-two-keyed-served-hash",
"pr": "#21436",
"head": "b53bea8da5",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "The seat's reading was confirmed: #21432 (b206403) added an exit-1 pin, written while the audit read two tables. This branch widens the audit to the decision-audit trail, so the merged tree reads three. origin/main (53fd35e) was merged INTO the branch as merge commit 8ca4966. There was no rebase and no force-push, and the merge was automatic: protocol.ts and engine.ts merged without conflict. The pin was then updated to stack both intents (b53bea8). The pre-existing reset-door pins and the conformance pin are byte-identical to main.",
"premise_reading": "Unchanged since patch round 1: no stored value persists a served token. This round adds no write path; the merge brings none that stores a version token.",
"superseded_pins": [
{
"file": "packages/cli/src/commands/migrate/preview-read-only.integration.test.ts (added by #21432)",
"title_before": "audit-metadata-bodies without --apply on a database that does not exist exits 1 with both tables counted unread",
"title_after": "audit-metadata-bodies without --apply on a database that does not exist exits 1 with every audited table counted unread",
"assertions_before": "exitCode 1; payload.apply false; report.failures toBe(2); report.scanned toBe(0); sorted byObject keys toEqual([sys_activity, sys_audit_log])",
"assertions_after": "audited = [sys_activity, sys_audit_log, sys_metadata_audit]; exitCode 1; payload.apply false; report.failures toBe(audited.length) (3); report.scanned toBe(0); sorted byObject keys toEqual(audited)",
"why": "Stacked intents. #21391: every audited table is counted unread. #21207: the audit set gains the decision-audit trail. No assertion was dropped or loosened. The #21391 comment above the case is kept, and a #21207 note is added at the case.",
"derivation": "The set is literal. The plugin exports no combined list of what the run reads: STORED_METADATA_BODY_AUDIT_OBJECTS covers only the two copy tables, METADATA_DECISION_AUDIT_OBJECT is not exported from the package index, and the runner composes its table list inline. A literal set fails loudly on any later widening; it cannot desync silently."
}
],
"control": "Control C1 via scripts/ablation-replace.mjs --delete, anchored on the decision-audit entry of the runner's table list in packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts. The anchor hit once (x1 to x0) and the blob moved 386a02f5484a to 62a90d7c9ff0. The CLI resolves plugin-audit to source through its vitest alias. The updated pin went red, 1 of 9: expected 2 to be 3, from the failures assertion. Restore: blob == HEAD (386a02f5484a), git diff HEAD empty, status clean.",
"tests": [
"Full build at the merge commit, before the test-only commit: turbo run build --filter=!@objectstack/docs --concurrency=2 under os-verify-lock, VERDICT command-exit 0 (6m30s on a shared box).",
"typecheck, each exit 0 through os-verify-lock: metadata-protocol (tsc --noEmit); objectql (tsc --noEmit, then scripts, then check:test-typecheck); plugin-audit (same three); cli (tsc --noEmit, then check:test-typecheck).",
"Suites at b53bea8, each through os-verify-lock with VERDICT exit 0. Commands are pnpm --filter PKG exec vitest run --maxWorkers=2 unless noted:",
"cli --project integration (queue population): 76 files, 687 passed, 2 skipped",
"cli --project unit: 248 files, 3552 passed",
"client: 50 files, 644 passed",
"metadata-protocol: 204 files passed, 3 skipped; 3077 passed, 19 skipped",
"plugin-audit: 38 files, 618 passed",
"The three named CLI files run explicitly with --reporter=verbose: preview-read-only.integration.test.ts 8 passed, 1 skipped; schema-migrate.one-shot-family.integration.test.ts 44 passed; delete-reset-carriers.test.ts 20 passed. Total 72 passed, 1 skipped.",
"Byte identity at HEAD, base 41a3c8d and origin/main alike: client meta-delete-item-carriers.test.ts 7950f3f1d6d3, cli delete-reset-carriers.test.ts b6f9c518f0ff, objectql publish-package-drafts-response-conformance.test.ts 63f687725a1d."
],
"gates": "At b53bea8. dispatch-gates --repo objectstack-ai/objectstack --commands derived 84 commands; all 84 ran and recorded exit 0. --ran: 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. pnpm lint on the whole repo: exit 0, 135s on a shared box. check:error-code-casing: exit 0.",
"mcp_calls": "0",
"api_writes": "1: POST /repos//issues/21207/comments (this os-dev-report) through the fleet-write relay. git push 7660d81..b53bea8, a fast-forward (7660d81 is an ancestor, exit 0); not a REST write. Reads: REST GET of the check-runs on b53bea8, once.",
"checks_after_push": "Read once at b53bea8, right after the push: 0 check runs registered yet. Not polled; CI convergence is the seat's.",
"open_questions": [],
"deviations": [
"The PR body is not edited; the text for the seat is in pr_body_edits_for_seat.",
"origin/main moved one more commit (086ad0a) after the merge. dispatch-gates reports that none of what it derives from was touched. No second merge was made.",
"No other file changed this round besides the merge and the one pin."
],
"files_changed_this_round": [
"merge commit 8ca4966 (origin/main 53fd35e into the branch, automatic)",
"packages/cli/src/commands/migrate/preview-read-only.integration.test.ts (one case: title, a #21207 note, the literal audited set)"
],
"pr_body_edits_for_seat": "In "Superseded pins updated", add a bullet: - The absent-database audit pin that #21432 added (dry run of the audit-metadata-bodies migration on a database that does not exist) counted two tables unread. The audit now also reads the decision-audit trail, so it counts every audited table: three, each named, none scanned, exit 1. A control that removes the decision-audit table from the run turns it red. In "Acceptance notes", "Serial constraint" bullet: also say origin/main was merged again (8ca4966, including #21432) and the pin above was stacked.",
"out_of_scope_findings": []
}
ACCEPT: PR #21436 at b53bea8da5 (exit two of the stored-metadata-body family: every door that serves a stored content hash serves it keyed, every door that takes a version token back compares it keyed, and copies carry no hash). Fixes #21207; landing through the queue
Contract review of record: PASS on b53bea8da5, by an isolated CONTRACT_REVIEW_TIER agent. The record is comment 5960845421 on the PR.
The agent reviewed all three heads.
Its FAIL on 7660d811a7 came from that head's red CI and was held, not posted. Every judgment that did not depend on the merge carried over.
Two patch rounds, both from CI:
On 1ad5a0099e: option A was falsified. With no crypto provider, the receipts served an empty version token. The SDK and the CLI drop an empty pin, so the pre-existing reset-door pins (the SDK's and the CLI's) failed open. The fix is the first patch round.
What replaces it: the doors key under a process-scoped ephemeral key while no provider is registered. A served token is never empty and never the stored hash. An empty, withheld, raw or stale token is refused with 409 METADATA_CONFLICT. A held token costs one 409 across a restart, or when a provider is first registered.
What it keeps: the ruling's security property, that no served value confirms withheld material offline.
The MCP stdio reader has no version-token door. It still omits the hash columns on a host with no provider.
The premise was measured: no stored value persists a served token.
The maintainer may overrule this by comment. The changeset and the PR body state the behaviour.
Clause-②: yes (narrowing). Six packages are bumped minor with the BREAKING banner, and ADR-0087 is not-required (no-migration-prescription). This is the family's precedent level. @objectstack/rest changes a comment only and carries no changeset.
Open questions, as judged:
Several processes with no provider: each process has its own key, so a token from one is refused by another. A, accept.os serve always registers a provider, and the cost is a loud 409, never a lost update. A key derived from a configured secret becomes a follow-up only if such a deployment is real.
Key placement:A, per process in metadata-protocol. It is the narrower change, and it fails closed for the MCP reader.
Member 14 (the change note) and the search narrowing:A, keep both.
Member 14 is the same oracle class on the same rows, so the family's enumeration grows from 13 to 14.
Diff and gates: the net diff is 22 files, +2765 / −138. check-governed-merges --pr 21436 reads not governed (22 paths, 2903 changed lines), and mergeable_state reads clean.
The token descriptions in the spec's protocol schema still say the token is currently emitted as an unkeyed hash. Acceptance notes. Carrier: the spec seat, as a docs-only change with the generated docs regenerated. The format is declared outside the contract.
The hash-column list is spelled in four packages. Acceptance notes. Its natural home is beside the body-column primitives in the spec kernel; carrier: the spec seat.
The MCP hash refusal does not name aggregations (no member serves the value). Acceptance notes: a wording gap, not a hole.
The first cut's validation skipped @objectstack/client and the CLI integration tier, because no source changed there. That was the seat's dispatch-order miss: both drive the real protocol. It is recorded on the seat post, not as a card.
Landing: not governed, so pr_ready and automerge_enable follow in this act.
Landed: PR #21436 → 713b0fa76d (exit two of the stored-metadata-body family: keyed served hashes and keyed inbound tokens, with no hash in any copy). Fixes #21207: the card is complete
Landing reading:713b0fa76d is on origin/main as a single-parent squash: 22 files, +2765 / −138. Its git patch-id --stable is 11c67d4b177d…, equal to the reviewed net diff at b53bea8da5.
The card: closed as completed by this merge. Both exits named in the card's title have now landed:
pm:dispatched is removed in the same act as this note.
On record (see the ACCEPT above): with no crypto provider, the doors key under a process-scoped ephemeral key instead of serving no hash. This is a recorded departure from ruling B's literal sentence. It keeps the ruling's security property and the landed reset-door contract.
Activity
objectstack-fleet commentedon Oct 1, 2026
Triage: first grade —
bug·security·priority:p1·domain:cli·area:access·pm:queue. Exit one joins the family's projection and pin; exit two's hash becomes keyed, not narrowedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T16:53Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only.Why p1. Both exits were measured with an administrator's credential, and members are refused. It is the family's grade (#21120, p1). Exit one serves credential material in cleartext, so it is first in the PR.
Routing. Exit one lands in
packages/mcp(domain:cli). Exit two lands in the metadata repository (domain:engine), declared as the same claim's cross-lane surface and dispatched vertically.The ruling (triage's; overturnable by the maintainer):
stored-metadata-body-family.pin.test.ts), so a future reader fails the pin instead of joining silently. ⛔ No second projection.Pins: each exit, as an administrator, serves no credential material and no unkeyed hash, with a credential-free body as the control.
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p1security, triage's first grade5936231568, filling a free slot under the maintainer's 「并发保持3」Session:
session_01VvcEokUG1tvVxkceYfR5XBAccount:
huangyiireneBranch:
claude/issue-21207-stored-body-two-exitsWorktree:
objectstack-issue-21207Domain:
domain:cliSeat:
domain:cli#1File surface (classes and positions only, ⛔ no request, field path or value):
packages/mcp/src/stdio-data-bridge.ts(its query and get paths, around:146/:353on097ef80270) andpackages/mcp/src/plugin.ts(the record resource, around:597).336e191441): it serves the projected body, or refuses. ⛔ No second projection.packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts, so a future reader fails the pin instead of joining silently.packages/metadata-protocol/src/sys-metadata-repository.ts, the row hash and the version it feeds, a cross-lane surface declared per triage. The served hash becomes keyed (HMAC under a server-held key), so the version still moves on every change and no offline verifier exists. ⛔ It is not narrowed to the projected body..changeset/21207-*.mdat the level the real diff takes.(stop on a breach outside these; explain in the report)
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable).Clause-②: no
Thread-read: 5936231568
Serial constraints cleared:
No open PR touches packages/mcp/src/**, packages/metadata-protocol/src/sys-metadata-repository.ts, the family pin or protocol.ts (the file lists of all 16 open PRs were read in this act, main 097ef80270). In flight on this seat: #20752 stage 3 (packages/runtime strings), disjoint. area:access in flight elsewhere: #21185, #21180, #21154 and #21079, on drivers, forms spec, plugin-audit and plugin-security, all disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 21207,
"status": "needs_decision",
"branch": "claude/issue-21207-stored-body-two-exits",
"pr": "#21228",
"session": "session_01VvcEokUG1tvVxkceYfR5XB — mode:subagent, the dispatching domain:cli seat session id",
"premise_still_valid": true,
"summary": "Exit one is delivered in draft PR #21228 (Part of #21207). The MCP stdio transport's engine-only reader (the stdio bridge's query, get and aggregate verbs, plus the record resource) now serves stored-metadata rows through the family's one projection (redactStoredMetadataRow, @objectstack/spec/kernel). It refuses the evaluate shapes (group, filter, sort, aggregate member on the stored body column) with the data door's INVALID_FIELD / 400. The family enumeration pin gains the two MCP rows; a new per-package pin classifies every bridge member and enumerates every engine read call site of packages/mcp/src on the syntax tree. Exit two was not built: it stopped at the claim's stop clause because the keyed hash needs a key source that does not exist. No existing keyed primitive reaches the metadata repository, so it needs either a new secret or a new keyed method on the crypto-provider contract. The decision is open_questions[0]; #21207 remains open for it. Premise measured live: the datasource body, affected by construction, is affected by measurement (cleartext to an administrator via the record resource and the bridge, member refused). The served content hash is an unkeyed sha256 over the whole stored body and equals the save receipt's version.",
"tests": "Head ae754f9. @objectstack/mcp pnpm test: Test Files 33 passed, Tests 366 passed (exit 0). @objectstack/mcp pnpm typecheck: exit 0, 'check:test-typecheck: OK' and the new test file is in the tsconfig.test.json program (listFilesOnly count 1). @objectstack/metadata-protocol vitest run: Test Files 200 passed / 3 skipped, Tests 2973 passed / 19 skipped (exit 0). @objectstack/metadata-protocol pnpm typecheck: exit 0, and the family pin is in its program. New pin src/stdio-data-bridge.stored-metadata-body.test.ts: 22 passed. Ablations: the fix was committed first, each mutation went through scripts/ablation-replace.mjs (anchor hit, blob changed), and each restore was proven 'blob == HEAD and git diff HEAD empty'. A1 (projection removed in serveStoredMetadataRow): 5 failed / 17 passed; red = the administrator no-credential pins (query, get, unjudgeable body, body-only projection, record resource), green = controls, refusals, member, teeth. A2 (evaluate refusal disabled): 9 failed / 13 passed; red = all 9 refusal pins, green includes the scalar-column control. A3 (an extra engine read behind a cast in plugin.ts): 1 failed / 21 passed. A4 (an unclassified bridge member): 1 failed / 21 passed. First attempts of A3 and A4 were no-ops: the replacement kept the anchor, so the tool refused and nothing ran; both were re-run with new anchors. A3 against the original name-based scan stayed green, so that scan was replaced by the AST scan and A3 was re-run red. No dist-reading committed test exists: the dogfood probe was private. The probe ran against a rebuilt mcp dist (marker count of the new function in dist: 3).",
"mcp_calls": "0 — no MCP GitHub tool was called; GitHub reads were single-card REST reads (issue #21207 and its comments, the #20790 dev report comment, PR #21228 read-back)",
"api_writes": "3 — all through scripts/pm/ tools over the fleet-write relay (each a POST /repos/objectstack-ai/objectstack/dispatches with the session token, executed by the relay as objectstack-fleet[bot]): (1) fleet-write/dispatch.mjs pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #21228, body read back identical 8720 bytes); (2) label-write.mjs --assign huangyiirene → POST /repos//issues/21228/assignees (read back: assignee matches); (3) post-stamped.mjs → POST /repos//issues/21207/comments (this report). git push is not REST.",
"open_questions": [
{
"question": "Exit two: the ruled keyed hash has no key source in the tree, and its at-risk population is the stored rows that already exist. Which key, and at which boundary? Measured: datasource save door refuses a credential in the body (422), and flows leave the body with #20790. So a hash keyed at WRITE time protects mainly flows written before #20790, while the existing stock and its history rows (the rows that actually carry credential material) keep serving the unkeyed sha256 unless they are re-keyed.",
"options": [
"A — write-time keyed hash under a NEW dedicated server secret (refuse production boot without it; dev/test a persisted local key), plus a one-off re-key migration of every existing row's checksum and previous_checksum. Cost: a new operator secret in every deployment; amending the repository contract's canonical-hashing invariant and the shared contract suite (metadata-core); the spec description of the history checksum; the stored-row checksum column bound (71 chars, a keyed prefix needs 76); the second producer in packages/metadata (DatabaseLoader) moved to the same function; rewriting append-only history; client-held versions answer one 409 after the switch.",
"B — serve-time keyed transform. Stored checksums stay canonical sha256 (contract, suite, both producers, FS layer and parent links untouched); every serve exit of a hash serves HMAC(key, stored hash), and every inbound version (If-Match / parentVersion, revert-to-version) is compared in that form. The key comes from the existing OS_SECRET_KEY through a new keyed method on the crypto-provider contract (spec contract widening, Clause-② yes; every provider implements it). No provider wired ⇒ the hash is withheld, never served raw. Cost: a hash-serving exit family to enumerate and pin (data door both tables, this MCP door, save receipt, /meta version and history, ETag), one contract method, and old clients' held versions answer one 409.",
"C — re-rule without a key: serve no hash for a row whose projection withheld material (the oracle exists only there), and keep serving the stored hash for every other row. Cost: those rows lose a served version, so their optimistic lock falls back to the existing last-write-wins path. Triage's 'keyed, not narrowed' ruling rejected hashing the projected body; this is a different narrowing and would need the ruling reopened."
],
"recommendation": "B, with C as the interim only if a contract widening is unwelcome now. 实际业务需求: the measured at-risk population is the existing stock and its history. B covers it on every exit without rewriting append-only history; A needs the re-key migration to reach it at all, and write-time keying alone protects almost nothing measured. 项目长远合理性: B keeps the version layer's canonical-hash contract intact and treats the keyed form as a serve-boundary concern, like the body projection, and the key stays with the existing secret owner (KMS-compatible). A changes the repository contract and runs two producers through a new secret. 防 AI 写错: B is one serve-time function plus an enumerated, pinned exit family, the pattern this body family already proved, and a missing provider fails closed. 创业阶段不扩散: B adds no operator secret and one contract method; A adds a secret, a migration and contract amendments; C adds nothing but reopens a ruling and degrades locking on exactly the credential-bearing rows."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — the generic data door's body-column filter refusal matches the exact column name, while this PR's door also refuses a dotted path into the body by its head segment. Whether the data door evaluates a dotted path into the stored body was read from source only and not measured, so it is not a finding. It is in PR #21228's Acceptance notes.",
"carrier: 承接者:无 · noted, not filed — packages/metadata's DatabaseLoader is a second producer of the stored-row and history checksum columns, with a different canonicalization (calculateChecksum) than the repository's hashSpec. Whether its save path is live in a production composition was not measured; it is listed in hash_consumers because exit two must move it too."
],
"gates": {
"head": "ae754f9e3a",
"derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands → 61 commands (exit 0; tree objectstack-ai/objectstack at ae754f9, change set 5 paths vs merge base bbcd20c)",
"ran": "61 of 61, each recorded 'COMMAND :: exit CODE', all exit 0 at ae754f9. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (8 packages without dist); it was re-run after building them and passed, exit 0. check:comment-mask-adoption first went red on a private comment stripper in the new test; fixed by moving the scan to the TypeScript AST, then exit 0.",
"reconcile": "dispatch-gates --ran: 61 derived, 61 run, 0 NOT-MEASURED (a DERIVED zero — all 61 recorded an exit code and none is 3)",
"lint": "pnpm lint (eslint . --no-inline-config, repo-wide, not narrowed): exit 0 at ae754f9, 139 s wall on a shared box",
"ci": "not awaited (in_progress at report time; CI convergence is the PM's)"
},
"deviations": [
"PR body first line is 'Part of #21207', not the dispatch's 'Fixes #21207': exit two is not built, so the merge must not close the card.",
"Clause-② is declared 'no (narrowing)', not the claim's 'no'. The diff narrows what a published door serves (projection instead of stored bytes) and accepts (evaluate shapes refused). This follows the family precedent (#21144 changeset): minor, BREAKING, ADR-0087 not-required (no-migration-prescription), and check:adr-0087-registration reads it '[BREAKING+bang+clause-②-narrowing]'.",
"Exit two stopped at the claim's stop clause (the key needs a new secret or a new contract method). No exit-two code was written, as the dispatch ordered.",
"Exit one's pins hold for credential material, the credential-free control and the member refusal. The 'no unkeyed hash' half is NOT MET: the stored checksum column is still served as stored and waits on open_questions[0].",
"The aggregate verb of the same stdio bridge is included in exit one. It is the same file and the same engine-only reader, and a group key on the body measured as serving the body.",
"The dispatch did not carry the four-axis decision framework. The recommendation uses the canonical copy in .claude/skills/pm-dispatch/SKILL.md 升级与决策, read once and not retained.",
"Measurement used a private probe test. It was copied into packages/qa/dogfood/test only for each run and removed right after (git status empty each time) and was never committed. Its readings are in the scratch folder issue-21207 (measure-before.json, measure-after.json).",
"The datasource credential case was measured on historical-stock rows, written straight to the two tables as the system, because the datasource save door refuses a credential in the body (measured 422).",
"Commits carry the model-free trailer pair from the dispatch and AGENTS.md. The harness attribution reminder asked for a model-named trailer, which was not used.",
"Labels documentation, size/l, tests and tooling on PR #21228 were applied by the repository's labelers, not by this run (the only label-write was the assignee)."
],
"files_changed": [
"packages/mcp/src/stdio-data-bridge.ts",
"packages/mcp/src/plugin.ts",
"packages/mcp/src/stdio-data-bridge.stored-metadata-body.test.ts",
"packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts",
".changeset/21207-mcp-stdio-stored-metadata-body.md"
],
"repro": {
"boot": "real boot (showcase + security + automation + SQLite file DB + connector plugins); administrator and member API keys minted through the key door and resolved the way the stdio door resolves them; before = origin/main 097ef80 build, after = branch build with rebuilt @objectstack/mcp",
"exit_one": [
{
"body": "flow body with credential",
"role": "administrator",
"before": "LEAK on record resource (resources/read), bridge get and query, both tables",
"after": "served as projection, no credential, on every path"
},
{
"body": "datasource body with credential (historical stock)",
"role": "administrator",
"before": "LEAK on record resource, bridge get and query, both tables",
"after": "served as projection, no credential, on every path"
},
{
"body": "credential-free body (flow and datasource)",
"role": "administrator",
"before": "served",
"after": "served byte-identical (same byte counts before and after)"
},
{
"body": "any",
"role": "member",
"before": "refused PERMISSION_DENIED on every path; the record resource answers an error text, no row",
"after": "same"
},
{
"body": "flow, datasource",
"role": "administrator",
"shape": "filter on the body column",
"before": "served — answered a guess (match vs no match)",
"after": "refused INVALID_FIELD / 400, engine not asked"
},
{
"body": "flow, datasource",
"role": "administrator",
"shape": "sort on the body; group by the body; aggregate filter on the body",
"before": "served (body in the answer / guess answered)",
"after": "refused INVALID_FIELD / 400"
},
{
"body": "flow, datasource",
"role": "administrator",
"shape": "aggregate member over the body",
"before": "refused by the engine INVALID_FIELD / 400",
"after": "refused here first INVALID_FIELD / 400"
},
{
"body": "flow, datasource",
"role": "member",
"shape": "evaluate shapes",
"before": "refused PERMISSION_DENIED",
"after": "refused INVALID_FIELD / 400 (before the engine, the data door's order)"
},
{
"body": "any",
"role": "administrator and member",
"shape": "tools query_records / get_record",
"before": "refused by the tool layer's system-object guard",
"after": "unchanged"
}
],
"exit_two": {
"served_hash_keyed": "NO — before and after: an unkeyed sha256 over the whole stored body (credential included), served on the generic data door and on this MCP door, equal to the save receipt's version (measured true)",
"version_moves_on_every_change": "YES today (the hash covers the whole body, credential included) — unchanged, nothing built",
"offline_verifier_defeated": "NO — not built (stopped at the stop clause)"
}
},
"hash_consumers": [
{
"where": "packages/metadata-protocol/src/sys-metadata-repository.ts:612-614 put()",
"compares": "caller-held parentVersion vs the stored checksum (optimistic lock)",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:620 put()",
"compares": "freshly computed hash of the incoming body vs the stored checksum (no-op short-circuit; needs a key stable across restarts and nodes)",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:485 getByHash() (ADR-0009 execution pinning), forwarded by metadata-core layered-repository.ts:96",
"compares": "a version looked up by history checksum; no production caller found in this checkout",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:785-828 delete()",
"compares": "parent lock on the stored checksum; tombstone records previous_checksum",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:922-997 promoteDraft() / dropPromotedDraftRow()",
"compares": "draft drained by its own checksum; parentVersion = active hash",
"cross_deployment": false
},
{
"where": "sys-metadata-repository.ts:1249 history read; :1926 rowToItem fallback",
"compares": "serves the parent chain (previous_checksum → parentHash); recomputes hashSpec(body) when a row has no checksum",
"cross_deployment": false
},
{
"where": "packages/metadata-protocol/src/protocol.ts:17673-17687 saveMetaItem, :18318 restore (parentVersion = history row checksum), :21898, :23031-23033 delete",
"compares": "parentVersion from the caller or the current hash vs the stored checksum; the save receipt echoes the version",
"cross_deployment": false
},
{
"where": "packages/rest/src/rest-server.ts:6992, :7222 (If-Match → parentVersion); packages/client/src/index.ts:806 (SDK If-Match); packages/cli/src/commands/meta/delete.ts (--if-match)",
"compares": "a client-held served version round-tripped to the same server",
"cross_deployment": false
},
{
"where": "packages/metadata/src/loaders/database-loader.ts:725-728, :1356-1366, :1404-1414 (calculateChecksum), :1002, :1145 (etag)",
"compares": "a SECOND producer of the same checksum / previous_checksum columns with a different canonicalization; no-op skip by equality; serves the checksum as an etag",
"cross_deployment": false
},
{
"where": "packages/metadata/src/metadata-manager.ts:3393",
"compares": "two versions' checksums side by side (version compare)",
"cross_deployment": false
},
{
"where": "packages/metadata-fs/src/repository.ts:261, :282, :361, :517, :530, :1004",
"compares": "the FS layer's own unkeyed hashSpec for file items; no FS-vs-DB hash comparison found (LayeredRepository probes each layer with its own hashes)",
"cross_deployment": false
},
{
"where": "packages/objectql/scripts/dry-run-hash-compat.ts",
"compares": "recomputes hashSpec(body) offline and compares with the stored checksum — an offline verifier by design",
"cross_deployment": "offline tool, same database"
},
{
"where": "packages/metadata-core/src/repository.ts:24 invariant 4; contract-suite.ts:207, :230, :270; packages/metadata-protocol/src/sys-metadata-repository.contract.test.ts",
"compares": "item.hash === hashSpec(item.body) — the repository contract every implementation (this one included) is tested against",
"cross_deployment": false
},
{
"where": "packages/spec/src/system/metadata-persistence.zod.ts:425; packages/metadata-core/src/objects/sys-metadata.object.ts:157-162 (maxLength 71); sys-metadata-history.object.ts:111-125 (maxLength 80)",
"compares": "declared shape: the history checksum is described as SHA-256 of the content; the stored-row column is bounded to a sha256-prefixed value",
"cross_deployment": false
},
{
"where": "cross-deployment consumers",
"compares": "none found in this checkout; the cloud repository (environment promotion, marketplace) was not available — NOT MEASURED",
"cross_deployment": "NOT MEASURED (cloud)"
}
],
"key_source": "None existing is reachable. The crypto provider contract (packages/spec/src/contracts/crypto-provider.ts) offers encrypt, decrypt, rotateKey and digest. Its digest may be plain SHA-256, and LocalCryptoProvider.digest is unkeyed sha256 (packages/services/service-settings/src/local-crypto-provider.ts:500). The provider is attached by the host after runtime.start (packages/cli/src/commands/serve.ts:5080-5092) into a private engine field (packages/objectql/src/engine.ts:3666), and the metadata repository cannot reach it. Its key material is OS_SECRET_KEY / OS_DEV_CRYPTO_KEY / a persisted file / an ephemeral test key, resolved inside service-settings. So a keyed hash needs a new secret, or a new keyed method on the crypto-provider contract plus a route from it to the hashing boundary. Either one triggers the claim's stop clause.",
"coexistence": "Write-time keying (option A): new rows store a keyed value and legacy rows keep sha256. The parent chain stays intact because previous_checksum copies the parent's stored value, so a keyed child points to an unkeyed parent. The first re-save of an unchanged legacy body mints one extra version unless no-op detection also accepts the legacy form. getByHash and history lookups keep matching stored values. Legacy rows keep serving their sha256, which is the offline verifier for exactly the stock at risk, until a re-key migration rewrites checksum and previous_checksum together. Serve-time keying (option B): nothing stored changes; legacy and new rows are served through the same transform, so the served chain stays consistent, and only versions clients held from before the switch answer one 409.",
"cleanup": "Worktree /home/user/objectstack-issue-21207 is clean (git status empty). After this comment posts: rm -rf its node_modules, then git worktree remove without --force. No server or monitor was started; the one gate batch that the harness moved to the background was waited on in the foreground with tail --pid and has exited."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
待维护者裁决:出口二(对外提供的内容哈希)要加密钥,但仓库里没有可用的密钥来源
domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T19:02Z状态安排: 出口一由 PR #21228 交付(
Part of #21207),正在复核与 CI 中,与本裁决无关、照常落地。它合并的同一笔里,本卡从pm:dispatched转needs-user-decision,退出活动队列,进决策箱。在那之前本卡仍在飞。一句话问题: 管理员读取元数据存储行时,每行都附带一个"版本号"。它是对整个存储体(含被隐去的凭据材料)直接算的 sha256。拿到它的人可以离线逐个猜凭据、对答案,猜中即知。分诊裁定改成"加密钥的哈希"(HMAC),但仓库里没有任何现成密钥能送到算哈希的地方。dev 按认领的停止条款停手,未写出口二的代码。
背景
5936231568第 2 条:对外提供的内容哈希改为 HMAC(服务端持有的密钥),⛔ 不缩窄成"只对投影体算哈希"。缩窄会让只改凭据的保存不再推动版本号,版本身份就坏了。5937050935的停止条款:若任一消费方跨部署比较哈希,或密钥来源需要新的秘密,就先停、先报。后一条触发了。5938425782:encrypt/decrypt/rotateKey/digest,本地实现的digest是不加密钥的 sha256;且提供者在运行时启动后才挂到引擎私有字段,元数据仓库够不到。Governing text
5936231568第 2 条(见上)。packages/metadata-core/src/repository.ts:24,仓库契约不变量 4:「Canonical hashing.item.hash === hashSpec(item.body)— always.」packages/spec/src/contracts/crypto-provider.ts:155-161,digest的说明是「SHOULD NOT reveal the plaintext (use HMAC or SHA-256 of canonical JSON)」。即契约允许不加密钥,所以不能依赖它来提供本安全性质。Clause-②: yes),走 spec 车道并需契约复审;选项 A 要修改上面的不变量 4。前提(各带复查命令)
git grep -nE "^\s*(digest|encrypt|decrypt|rotateKey)\b|hmac" origin/main -- packages/spec/src/contracts/crypto-provider.ts:今天只命中四个方法。digest不加密钥。git grep -n "createHash('sha256')" origin/main -- packages/services/service-settings/src/local-crypto-provider.ts::501。git grep -n "maxLength: 71" origin/main -- packages/metadata-core/src/objects/sys-metadata.object.ts。repro.exit_two与key_source段。repro.exit_one。选项 × 真实代价
OS_SECRET_KEY,经加密提供者契约新增的带密钥方法取得。没挂提供者时不提供哈希,绝不提供原值。Clause-②: yes),所有提供者都要实现。要枚举并钉住"提供哈希的出口族":数据入口两张表、本 MCP 入口、保存回执、/meta版本与历史、HTTP 缓存校验值。切换后旧版本号各撞一次 409。业务含义直译
四轴(业务立场)
os-decision-facets
Prior rulings read:
HMAC|keyed hashindocs/adr→ 4 hits (all webhook / notification signing, unrelated); ADR none; thread:5936231568(triage ruling 2)推荐:B。回退:C,作为过渡,仅当此刻不愿放宽 spec 契约时选。
resourceVersion也是不透明令牌,不是内容摘要。B 正是这个形态。置信缺口:
digest改为带密钥,而不加新方法;但契约允许digest不加密钥,依赖它就是"声明而未强制",本席不荐。裁后执行
Clause-②: yes卡,给加密提供者契约加带密钥的方法,并经契约复审。你要做的(一个动作): 回复 A、B 或 C(可附条件)。
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT: PR #21228 at
ae754f9e(exit one of #21207: the MCP stdio transport's engine-only reader joins the stored-metadata-body family).Part of: exit two waits on the maintainer (5938508053)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T19:13Z5937050935declaresClause-②: no, and the PR declaresno (narrowing). A narrowing is not a Clause-② widening.packages/spec/src/**and no governed path.mainin5e5ce48cef.5936231568item 1:queryandgetand the record resource serve through the family's one projection,redactStoredMetadataRowfrom@objectstack/spec/kernel. No second projection is written.INVALID_FIELD/ 400 after the exposure gate and before the engine. This covers the call's filter and each aggregation's own filter. A path whose head is the body column counts as the column.create/update/removethere. This is pinned.stored-metadata-body-family.pin.test.tsgains the seam and refusal rows for@objectstack/mcp(6 → 8).os-dev-report5938425782, real boot, before vs after):The datasource body, which triage asked to measure, was affected and is now covered.
minorfor an accept-set narrowing, as in the family's precedent PR fix(security): close out the stored-metadata-body family — project or refuse every further read/copy/evaluate exit #21144);All hold. "No export or published type changes" holds: the new helpers are exported from
stdio-data-bridge.ts, butsrc/index.tsdoes not re-export that module, and the package publishes only.. Theadr-0087: not-requiredmarker reads as the gate expects.ae754f9e: 34 names, 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 red;Lint & Repo GatesandTypeScript Type Checkaresuccess.git merge-treeagainstorigin/main: clean against62b90d74f7, and nomaincommit since the merge base touchespackages/mcp, the family pin or@objectstack/spec/kernel.check-governed-merges --pr 21228: NOT governed, +805 / −9.main, first linePart of #21207.5938508053.packages/metadata's database loader) uses a different canonicalization. Whether it is live was not measured. Acceptance notes; it is input to exit two's execution.pm:dispatchedtoneeds-user-decision, as5938508053states.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed: exit one, PR #21228 →
3ddd3d0c4a(the MCP stdio transport's engine-only reader serves the stored body only as its type's read projection and refuses to evaluate it). APart oflanding: exit two waits on the maintainerdomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T19:45Z3ddd3d0c4ais onorigin/mainas a single-parent squash on62b90d74f7, 5 files, +805 / −9. Its diff equals the PR's net diff at headae754f9eline for line, once theindexand hunk-header lines are set aside.5938508053: A, B or C, with the seat recommending B.pm:dispatched→needs-user-decision, and the assignee is cleared, in the same act as this note. The card leaves the active queue until the maintainer answers. The seat's execution resumes from the ruling's "裁后执行" section.Generated by Claude Code
35 remaining items
test(metadata-protocol): the served-content-hash pin's where-matcher …
objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{
"issue": 21207,
"status": "done",
"branch": "claude/issue-21207-exit-two-keyed-served-hash",
"pr": "#21436",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Exit two of #21207 under ruling B and forks A/A/A, one draft PR for the whole family. Every door that served a metadata body's stored content hash now serves the crypto provider's keyed digest of it (nothing with no provider); every inbound version token is compared in keyed form against the current head and the stored value is handed to the repository lock (raw or stale: 409 METADATA_CONFLICT; no provider: every token refused 409). Filter, sort and group on the two stored content-hash columns are refused 400 INVALID_FIELD before the engine at the data door, the MCP stdio reader and the analytics door; write-time copies (ledger snapshot and diff, activity copy, decision-audit note) carry no hash, and the extended os migrate audit-metadata-bodies (dry run default, idempotent) rewrites copies at rest while the version history stays the lineage. The after-measurement found member 14 (the version history change note quoting the stored hash on a message-less draft promotion), closed in the same PR and flagged for the contract review.",
"tests": [
"Red first (pins committed on the unfixed tree 8c610b8): metadata-protocol 25 failed / 5 passed; mcp 11 failed / 3 passed; plugin-audit 14 failed / 88 passed; service-analytics 6 failed / 7 passed. Every red a door serving or accepting the stored value; controls green. Member-14 pins and the decision-note copy pin were written after the fix; their red is shown by ablation legs L06, L10, L15, L17.",
"Green at the fix (all through os-verify-lock, VERDICT command-exit 0): metadata-protocol full suite 3013 passed (re-run on touched files after the main merge); objectql 7358 passed; rest 4982; runtime 5081; mcp 380; plugin-audit 598; service-analytics 3793; cli unit project 3489 passed; cli migrate preview integration file 6 passed 1 skipped (live PG cell); dogfood 17 affected files passed.",
"typecheck exit 0: metadata-protocol, objectql, mcp, plugin-audit, service-analytics, rest, cli.",
"Ablation: 17 legs via scripts/ablation-replace.mjs on the committed fix, anchor hit once, blob changed, targeted pin red, restore proven blob == HEAD blob and git diff HEAD empty (see ablations).",
"Gates at 1ad5a00: dispatch-gates --repo objectstack-ai/objectstack --commands derived 84, all 84 run, all exit 0; --ran reconciled 84 derived / 84 run / 0 NOT-MEASURED / 0 UNRUN; check:error-code-casing 0; check:nul-bytes 0; pnpm lint (whole repo) exit 0."
],
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "3 — all through the fleet-write relay: POST /repos/objectstack-ai/objectstack/pulls (draft pr_create, #21436, read back 12992 bytes sent / 12992 stored, identical); POST /repos//issues/21436/assignees (label-write --assign huangyiirene, read back matches); POST /repos//issues/21207/comments (this os-dev-report). git push is not a REST write. Reads: one REST GET of check-runs on the head.",
"open_questions": [
{
"question": "No crypto provider registered: the receipts' required version token is the empty string (it names nothing and every inbound comparison refuses it). The response schemas declare the token a required string, and spec is outside the claimed surface.",
"options": [
"A keep the empty token (this PR)",
"B spec follow-up making the receipt version optional so a provider-less deployment omits it"
],
"recommendation": "A in this PR, B as a spec-seat follow-up if the contract review prefers absence to an empty sentinel; A is already fail-closed on every inbound door."
},
{
"question": "Member 14 (history change note quoting the stored hash) is outside the ruling's literal enumeration of members 1 to 13; it is closed here (hash-free publish message, keyed serve of pre-existing notes, evaluate refusal at three doors, copies withheld, migration).",
"options": [
"A accept as a family member in this PR",
"B split into its own card"
],
"recommendation": "A, same oracle class on the same row, same doors and the same seams; splitting would ship a known hash exit beside a fix that claims the family."
},
{
"question": "Data-door search over the two stored-metadata tables also stops scanning the stored body column (a released #21120-class search oracle on the same row, touched by this narrowing).",
"options": [
"A keep in this PR (declared)",
"B revert the body-column half and file it separately"
],
"recommendation": "A, the narrowing is one list over the unsearchable columns and leaving the body in it keeps a prefix oracle on withheld material."
}
],
"out_of_scope_findings": [
"carrier: spec seat, at the contract review of #21436 · the spec descriptions of the save, publish and batch-publish tokens still say the token is currently emitted as an unkeyed hash; the format is declared outside the contract, prose drift only · noted in Acceptance notes, not filed",
"carrier: spec / kernel seat · the hash-column list's natural home is beside the body-column primitives in the spec kernel module; metadata-protocol, mcp, plugin-audit and service-analytics each hold a copy, metadata-protocol's pinned equal to the object definitions, the others pinned by behaviour tests · noted in Acceptance notes, not filed",
"carrier: 承接者:无 · realtime record events would carry the two hash columns; out of scope by the ruling, no public channel route in this repository · noted in PR body, not filed",
"carrier: 承接者:无 · metadata-core base conflict text prints both stored values; no door serves it (every door converts the conflict, the revert door withholds undeclared failures) · noted in Acceptance notes, not filed"
],
"exit_family": [
"1 save receipt token · packages/metadata-protocol/src/protocol.ts:18148 saveMetaItem via receiptVersion (packages/metadata-protocol/src/protocol.ts:15817) · keyed; empty with no provider",
"2 publish receipt token · packages/metadata-protocol/src/protocol.ts:19087 publishMetaItem · keyed",
"3 package batch-publish tokens · packages/metadata-protocol/src/protocol.ts:20635 publishPackageDrafts · keyed per element, stored value stays internal",
"4 rollback receipt token · packages/metadata-protocol/src/protocol.ts:22913 rollbackMetaItem · keyed",
"5 history event hash and parent hash · packages/metadata-protocol/src/protocol.ts:18806-18812 historyMetaItem · keyed per event; null with no provider",
"6 conflict refusal text and attributes · packages/metadata-protocol/src/protocol.ts:15856 metadataConflictRefusal, used at 18226 (save), 19365 (publish promote), 22921 (rollback), 23546 (reset/delete) · keyed values or (withheld) / null",
"7 decision-audit note of a conflict · packages/metadata-protocol/src/protocol.ts:15917 optimisticConflictAuditEntry · names no hash, each side (withheld) or null",
"8 the two hash columns on both stored-metadata tables · packages/metadata-protocol/src/protocol.ts:12008 findData, 12142 getData via packages/metadata-protocol/src/metadata-redaction.ts:872 and 894 · keyed; omitted with no provider",
"9 evaluate shapes on those columns · packages/metadata-protocol/src/protocol.ts:11901 findData via packages/metadata-protocol/src/metadata-redaction.ts:927; search narrowing packages/metadata-protocol/src/protocol.ts:11195 / 11765 via packages/metadata-protocol/src/metadata-redaction.ts:983 and 994 · 400 INVALID_FIELD naming usable columns",
"10 MCP stdio engine-only reader · packages/mcp/src/stdio-data-bridge.ts:492 refusal, :550 serve, wired :691-720 and :802; packages/mcp/src/plugin.ts:577 and :629 (record resource) · keyed; group, filter, sort refused INVALID_FIELD",
"11 audit ledger copies · packages/plugins/plugin-audit/src/audit-writers.ts:1439; migration packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts:114 and :288 · dropped at write time; at rest via migration",
"12 activity copies · audit-writers.ts:1439; stored-metadata-body-migration.ts:307 · same as 11",
"13 analytics members on those columns · packages/services/service-analytics/src/stored-metadata-body-refusal.ts:86 · 400 INVALID_FIELD in either role",
"14 history change note · packages/metadata-protocol/src/protocol.ts:19353 promoteDraftForPublish states a hash-free message; served keyed by packages/metadata-protocol/src/metadata-redaction.ts:835 (history packages/metadata-protocol/src/protocol.ts:18812, data door, MCP); evaluate refused at the three doors; copies audit-writers.ts:1441 and :1448; migration stored-metadata-body-migration.ts:134 and :186 · keyed or (withheld); history row not rewritten",
"engine accessor · packages/objectql/src/engine.ts:8578 getKeyedDigest beside setCryptoProvider · additive, read at each use, hands out the digest only",
"family pin · packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts · 23 surface rows with dispositions seam / keyed / withheld / refuses, plus a tooth holding the hash-column list equal to the object definitions"
],
"inbound_paths": [
"save door (/meta save and the runtime dispatcher save): packages/metadata-protocol/src/protocol.ts:18012 saveMetaItem reads the current head and resolves the sent token through storedParentForToken (packages/metadata-protocol/src/protocol.ts:15831); a keyed match hands the stored value to the repository lock; a raw stored hash or stale token: 409 METADATA_CONFLICT; no provider: 409 METADATA_CONFLICT (unverifiable)",
"reset door: packages/metadata-protocol/src/protocol.ts:23444 deleteMetaItem, same comparison and same refusals",
"in-process only: migrateStoredMetadata passes the stored parent through an internal request member (packages/metadata-protocol/src/protocol.ts:17988, :18683); the REST and dispatcher doors build their save request field by field and never spread the body, so no door reaches it",
"publish and rollback doors take no caller token; their race conflicts go through metadataConflictRefusal"
],
"evaluate_refusals": [
"data door: filter, sort, group on either hash column or the change note: 400 INVALID_FIELD before the engine; explicit search fields naming one: 400 INVALID_FIELD; auto-default search no longer scans them or the body column",
"MCP stdio reader: query where / orderBy and aggregate groupBy on the same columns: INVALID_FIELD",
"analytics door: dimension or measure on the same columns in either role: 400 INVALID_FIELD",
"member role before and after: data door 403 PERMISSION_DENIED, history 403, ledger 403, analytics 403 PERMISSION_DENIED, MCP PERMISSION_DENIED"
],
"copies": {
"write_time": "after the change, ledger and activity copies and decision-audit notes written on a real boot carry no hash (0 rows)",
"migration_cli_on_a_base_written_db": "before: ledger 25 of 38 with a hash, activity 25 of 38, decision notes 1; dry run exit 0: unchanged, reports 53 rows to rewrite; --apply --yes exit 0: ledger 0 of 39, activity 0 of 39, notes 0; second dry run exit 0: 0 to rewrite; history lineage keeps its 9 stored hashes",
"stock_db_unmigrated": "served copies still carry the hash until the operator runs the migration (the ruled path)"
},
"repro": {
"composition": "showcase + automation + SQLite file database + audit plugin + three connector plugins; admin and member API keys minted through the key door (201 / 201); local crypto provider registered as os serve does",
"before_base_ecb6ca0258": "receipts equal the stored head; history serves stored hashes; raw stored hash sent back accepted 200 on save and reset doors; 409 carries the stored hash; data door serves stored values and a right guess confirms; filter right 1 row / wrong 0; group serves stored; search matches a hash prefix and a credential prefix; ledger, activity, notes, analytics and MCP serve stored",
"after_this_branch": "all served tokens and columns keyed, stable across reads, moved by a credential-only change; raw inbound 409 METADATA_CONFLICT; served token sent back 200; evaluate shapes 400 INVALID_FIELD; search narrowed; copies hash-free at write time; MCP keyed and evaluate refused"
},
"gates": "at 1ad5a00: 84 derived by dispatch-gates --repo objectstack-ai/objectstack --commands, 84 run, every exit 0; --ran: 84 / 84 / 0 NOT-MEASURED / 0 UNRUN; pnpm lint whole repo exit 0 (no narrowing needed); hygiene: engine-double-contract --write ledger, objectql-double-limit (find double holds the bound), where-matcher (combinators refused), dispatcher-error-vocabulary (operator-form read), error-code-casing (persisted vocabulary marked), dual-build-cjs-loads (missing builds made)",
"deviations": [
"member 14 (history change note) added beyond the literal 1 to 13 enumeration",
"data-door search narrowing also removes the stored body column (released #21120-class hole touched by this narrowing)",
"test and ledger edits outside the literal surface: objectql batch-publish conformance pin (now registers a provider), two decision-note pins in metadata-protocol (now assert withheld), scripts/engine-double-contract.pinned.json via --write",
"the migration also rewrites decision-audit notes (same copy class, fork three A)",
"rest-server.ts: one comment only; runtime meta.ts and metadata-core errors.ts untouched",
"no-provider receipts carry the empty token (open question 1)",
"member-14 and decision-note copy pins shown red by ablation, not red-first",
"origin/main merged once into the branch (41a3c8d, includes #21377; no hash exit added)"
],
"files_changed": "22 files, +2663 / -137 vs 41a3c8d: .changeset/21207-keyed-served-content-hash.md; packages/cli/src/commands/migrate/audit-metadata-bodies.ts; packages/mcp/src/plugin.ts; packages/mcp/src/stdio-data-bridge.ts; packages/mcp/src/stdio-data-bridge.stored-content-hash.test.ts; packages/metadata-protocol/src/metadata-redaction.ts; packages/metadata-protocol/src/protocol.ts; packages/metadata-protocol/src/protocol.served-content-hash.test.ts; packages/metadata-protocol/src/protocol.data-door-stored-content-hash.test.ts; packages/metadata-protocol/src/protocol.lifecycle-audit-rows.test.ts; packages/metadata-protocol/src/protocol.package-publish-audit-rows.test.ts; packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts; packages/objectql/src/engine.ts; packages/objectql/src/publish-package-drafts-response-conformance.test.ts; packages/plugins/plugin-audit/src/audit-writers.ts; packages/plugins/plugin-audit/src/audit-writers.test.ts; packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts; packages/plugins/plugin-audit/src/stored-metadata-hash-migration.test.ts; packages/rest/src/rest-server.ts; packages/services/service-analytics/src/stored-metadata-body-refusal.ts; packages/services/service-analytics/src/stored-metadata-body-refusal.test.ts; scripts/engine-double-contract.pinned.json",
"ablations": [
"L01 receipt served raw: 8 of 11 red",
"L02 raw token accepted inbound: 2 of 11",
"L03 history served raw: 3 of 11",
"L04 conflict carries the stored hash: 2 of 11",
"L05 note carries the token: 1 of 11",
"L06 publish door states no message: 1 of 11",
"L07 data-door columns served raw: 5 of 27",
"L08 data-door evaluate shapes unrefused: 12 of 27",
"L09 search not narrowed: 5 of 27",
"L10 quoted hash in a note served raw: 2 of 38",
"L11 MCP columns served raw: 3 of 16",
"L12 MCP evaluate shapes unrefused: 8 of 16",
"L13 analytics unrefused: 7 of 14",
"L14 writer copies the hash: 4 of 93",
"L15 writer copies a decision note hash: 1 of 93",
"L16 migration keeps the columns: 7 of 12",
"L17 migration keeps a note hashes: 5 of 12",
"every leg restored: blob == HEAD blob, git diff HEAD empty"
],
"checks_after_push": "read once at head 1ad5a00, not polled: 31 check runs, 11 success (among them Check Changeset, Check PR Size, Governed Surface Queue Guard, the claim and single-writer guards), 3 skipped, 17 in_progress (Build Core, Lint and Repo Gates, Test Core 1-6, Type Check x4, Dogfood x4, Temporal Conformance); no completed failure. CI convergence is the PM's."
}
fix(metadata-protocol)!: with no crypto provider, version tokens are …
objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{
"issue": 21207,
"status": "done",
"round": "patch round 1 (CI red on 1ad5a00: client and CLI reset-door pins)",
"branch": "claude/issue-21207-exit-two-keyed-served-hash",
"pr": "#21436",
"head": "7660d811a7",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "The seat's reading was confirmed by measurement. With no crypto provider, the first cut served an empty version token. The SDK and the CLI both drop an empty pin, by design and pinned, so a pinned reset went out unpinned and the lock failed open. Fix (one commit, 7660d81): while no provider is registered, the protocol keys tokens under a process-scoped ephemeral key (32 random bytes drawn on first use, held only in module memory). When a provider is registered, its key is read per use. Tokens are always served, differ per content, are never the stored hash, and an empty or withheld token sent back is refused with 409. The no-provider refusal branch is removed. The batch-publish conformance pin, which the first cut had re-composed, is restored to its base bytes. Both reset-door pins pass unedited.",
"premise_reading": [
"Ruling premise: no stored value persists a SERVED (keyed) token. Verified, it holds.",
"Save door: protocol.ts saveMetaItem resolves the sent token to the STORED head (storedParentForToken) before the repository is asked. The repository persists only stored hashes, as current and parent.",
"Reset door: deleteMetaItem does the same, and revertCommit passes the stored head internally.",
"Version history rows store the stored hash and stored parent. The change note is now hash-free; old notes are served keyed on read, not rewritten.",
"Decision-audit rows: the note is (withheld) or null, never a token (optimisticConflictAuditEntry).",
"Ledger and activity copies drop the hash columns at write time.",
"Package publish: the per-element token is computed only into the response (receiptVersion at published.push). The commit record holds items and seqs, no token. The audit note names package and commit only.",
"Rollback takes an integer history version, not a token.",
"The internal migration passes the stored parent in-process (storedParentVersion), never a served token.",
"CLI: os meta delete takes the pin from the operator per call and stores nothing. SDK: tokens are held in memory only; no browser storage in packages/client/src.",
"MCP: no metadata write door takes a version token.",
"A served token is therefore only ever held by a caller, and losing the key costs at most one 409 on a held token. Sister-repo clients (console, cloud) are not in this tree: NOT MEASURED, reason: not checked out here; for them the ruled cost is one 409."
],
"tests": [
"Prerequisite builds (os-verify-lock, VERDICT command-exit 0): client dependency closure, 34 packages; the 26 CLI dependencies that lacked a dist; @objectstack/cli itself.",
"Pins on the fix, as written: client src/meta-delete-item-carriers.test.ts 20 passed of 20; cli --project integration src/commands/meta/delete-reset-carriers.test.ts 20 passed of 20. Both are byte-identical to base 41a3c8d: client blob 7950f3f1d6d3, cli blob b6f9c518f0ff.",
"Full suites at 7660d81, each through os-verify-lock with VERDICT exit 0. Commands are pnpm --filter PKG exec vitest run --maxWorkers=2 unless noted:",
"metadata-protocol: 3031 passed, 19 skipped",
"client: 644 passed of 644",
"cli --project integration (queue population): 638 passed, 2 skipped",
"cli --project unit: 246 files, 3523 passed. Two files failed only because the cli dist was not built (prerequisite, not a red). After pnpm --filter @objectstack/cli build they were re-run: test/published-subpath-console.pin.test.ts and test/published-subpath-hook-body.pin.test.ts, 29 passed",
"rest: 4985 passed, 323 skipped",
"mcp: 382 passed",
"plugin-audit: 599 passed",
"service-analytics: 3802 passed, 89 skipped",
"objectql: 7365 passed, including the restored conformance pin, 15 of 15",
"plugin-hono-server, which sends the version header: 324 passed",
"metadata, the three repository-level files that send a version: 9 passed",
"cli nightly tier, OS_TEST_TIERS=nightly --project integration test/migrate-meta.e2e.test.ts (the internal stored-parent path): 19 passed",
"No other nightly-tier CLI file saves or reads a metadata version token (grep over the 78 e2e/live files: zero hits).",
"Targeted first, then: metadata-protocol protocol.served-content-hash, protocol.data-door-stored-content-hash and stored-metadata-body-family pins, 50 passed.",
"typecheck: metadata-protocol tsc --noEmit exit 0. No other package's source changed this round."
],
"ablations": [
"A1, source, metadata-protocol aliased to src. storedHashDigest falls back to an empty-token digest. Client real-door pin: 3 of 20 red, the exact CI failures (expected '' not to be '' twice; 'expected the stale scoped reset to be refused'). Protocol pins: 5 of 41 red. Restored: blob == HEAD, git diff HEAD empty.",
"A2, first attempt: a no-op. ablation-replace refused it because the replacement contained the anchor (anchor count 1 to 1). It restored automatically and the reading is void.",
"A2b, source. storedParentForToken reads an empty or withheld token as no pin. Served-content-hash pin: 2 of 14 red. Restored: blob == HEAD.",
"A3, dist. The empty-token mutation was built into the metadata-protocol dist; preflight found the marker in 2 built files. Base conformance pin in objectql: 1 of 15 red (expected 0 to be greater than 0). Restore leg: rebuilt, marker absent from all 24 files, tree clean, 15 of 15 green.",
"A4, dist. Same mutation, CLI integration pin: 6 of 20 red, the exact CI six. Restore leg: rebuilt, marker absent, tree clean, 20 of 20 green."
],
"gates": "At 7660d81. dispatch-gates --repo objectstack-ai/objectstack --commands derived 84 commands. The run before the container restart was killed; its partial record (56 lines) was set aside unread. The run after the restart recorded 82 commands at exit 0 and two at exit 3 (PREREQUISITE NOT MET): check:dual-build-cjs-loads, for missing dists, and check:i18n-coverage, because one example config could not load an unbuilt connector. Both were re-run after the dists existed, exit 0. --ran: 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. check:error-code-casing exit 0; check:nul-bytes exit 0 (in the derived set). pnpm lint on the whole repo: exit 0, 135s on a shared box.",
"dogfood": "NOT MEASURED this round. Reason: dogfood boots with a crypto provider registered, so the changed branch (the no-provider fallback) is off its path. It ran green in round one; CI runs it.",
"mcp_calls": "0",
"api_writes": "1: POST /repos//issues/21207/comments (this os-dev-report) through the fleet-write relay. git push 1ad5a00..7660d81 is not a REST write. Reads: REST GET of the check-runs on 7660d81, once.",
"checks_after_push": "Read once at 7660d81, right after the push: 0 check runs registered yet. Not polled; CI convergence is the seat's.",
"open_questions": [
{
"question": "Option A (an empty token with no provider) was falsified by CI and replaced by the ephemeral key, as ruled. One residual: on a multi-process deployment with no crypto provider, each process has its own key. A token served by one process is refused with 409 by another, so a client pinning against a load-balanced pool gets spurious 409s. os serve and the verify harness always register a provider, so this applies only to hosts that register none.",
"options": [
"A accept, and document "register a crypto provider for multi-process"",
"B a follow-up that derives the fallback key from a configured secret when one exists"
],
"recommendation": "A for this PR: the ruled cost is a loud 409, never a silent lost update. B only if a provider-less multi-process deployment is real."
},
{
"question": "Placement of the key: the ruling said "per engine". It is placed per process, in metadata-protocol beside the served-hash helpers, and read through the protocol's storedHashDigest. engine.ts is unchanged this round. Protocols in one process (per-environment) therefore answer one another's tokens, and a host engine without the accessor is covered too.",
"options": [
"A keep per process (this PR)",
"B move it into the engine accessor, so the MCP stdio reader also serves keyed columns with no provider instead of omitting them"
],
"recommendation": "A: it is the narrower change and it fails closed. With no provider, MCP omission is the safe direction, and MCP has no version-token door."
},
{
"question": "Carried from round one for the contract review: member 14 (change note), and the body-column search narrowing.",
"options": [
"A keep both in this PR",
"B split either"
],
"recommendation": "A, as argued in the round-one report."
}
],
"deviations": [
"PR body NOT edited. The coordinator asked for the open-question section to be updated, but the os-dev contract says the dev writes the PR body once, at creation, never PATCHes it, and names post-hoc edits for the seat to write; the contract wins on conflict. That conflict is stated here, not silently resolved. The exact replacement text is in pr_body_edits_for_seat. The first two lines are untouched.",
"The changeset was updated: consequence 1 now describes the ephemeral key, the one 409 after a restart or a provider registration, and the refusal of empty or withheld tokens. "What else changes" now says the MCP reader omits the columns with no provider. The ADR-0087 marker text was adjusted.",
"The ephemeral key is process-scoped in metadata-protocol, not per engine (open question 2). engine.ts unchanged.",
"Pre-existing pin restored: objectql publish-package-drafts-response-conformance.test.ts is back to base bytes (63f687725a1d). The first cut had added a provider to its composition, the pattern this round prohibits.",
"My own round-one pins changed (new in this PR, not pre-existing): the no-provider cases in protocol.served-content-hash.test.ts and protocol.data-door-stored-content-hash.test.ts now assert the keyed, non-empty, per-content, process-scoped behaviour and that a provider registered later moves tokens. A new case pins that an empty or withheld token is refused with a provider registered. One header line in the family pin was updated.",
"Container restart at about 17:52Z killed the first gate run. Its partial record was treated as unread and every gate was re-run. Suites and ablations had all completed, and been recorded, before the restart.",
"origin/main is at least 15 commits ahead (dispatch-gates note). No merge was made this round."
],
"files_changed_this_round": [
".changeset/21207-keyed-served-content-hash.md",
"packages/metadata-protocol/src/metadata-redaction.ts (ephemeralStoredHashDigest; doc text)",
"packages/metadata-protocol/src/protocol.ts (storedHashDigest never undefined; receiptVersion; storedParentForToken refuses empty; unverifiable branch removed; comments)",
"packages/metadata-protocol/src/protocol.served-content-hash.test.ts",
"packages/metadata-protocol/src/protocol.data-door-stored-content-hash.test.ts",
"packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts (header text)",
"packages/objectql/src/publish-package-drafts-response-conformance.test.ts (restored to base)"
],
"pr_body_edits_for_seat": "R1. In "Acceptance notes", replace the bullet that starts "No crypto provider registered." with:\n\n- No crypto provider registered (option A falsified by CI, replaced). The first cut served an empty version token on a host with no crypto provider. CI falsified that: two real reset-door pins, one in the SDK and one in the CLI, showed that every save then handed out the same empty token. A client that sends no pin for an empty token turned a pinned reset into an unpinned one, so the optimistic lock failed open. Replaced in this PR: while no provider is registered, the doors key under a process-scoped ephemeral key (32 random bytes drawn on first use, never written, logged or served). A token is always served, differs when the content differs, and is never the stored hash. An empty or withheld token sent back is refused with
409 METADATA_CONFLICT, never read as "no pin". A token held across a restart, or across a provider's first registration, is refused once with the same 409. No stored value carries a served token, so nothing persisted dies with the key. The MCP stdio reader has no version-token door; it still omits the hash columns on a host with no provider.\n\nR2. In "Superseded pins updated", replace the third bullet ("One batch-publish conformance pin asserted a non-empty token with no provider registered. It now registers a provider and asserts the keyed form.") with:\n\n- The batch-publish conformance pin asserts a non-empty token with no provider registered. The first cut changed its composition. It is back to its base bytes and passes as written.\n\nR3. In the exit-family table, the Disposition cells of rows 5 and 8 read "keyed per event;nullwith no provider" and "keyed; omitted with no provider". Replace them with "keyed per event" and "keyed".\n\nR4. Append after the "Ablations" table:\n\nPatch round (CI falsified option A). The fix lands at7660d811a7. Validation and ablation results are in the os-dev-report for this round. The SDK and CLI reset-door pins pass unedited. Restoring the empty token, with dist rebuilt, turns them red again: 3 of 20 and 6 of 20, the exact CI failures.\n","out_of_scope_findings": [
"carrier: 承接者:无 · the first cut skipped @objectstack/client and the CLI integration tier because neither package's source changed, although both drive the real protocol and the served token's wire value changed. The import-er rule (the public face changed, so its test is owed) applied and was missed. Noted, not filed."
]
}
test(cli): the absent-database audit pin counts every audited table, …
objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{
"issue": 21207,
"status": "done",
"round": "patch round 2 (CI red on the merge of 7660d81 with main: the #21432 absent-database audit pin)",
"branch": "claude/issue-21207-exit-two-keyed-served-hash",
"pr": "#21436",
"head": "b53bea8da5",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "The seat's reading was confirmed: #21432 (b206403) added an exit-1 pin, written while the audit read two tables. This branch widens the audit to the decision-audit trail, so the merged tree reads three. origin/main (53fd35e) was merged INTO the branch as merge commit 8ca4966. There was no rebase and no force-push, and the merge was automatic: protocol.ts and engine.ts merged without conflict. The pin was then updated to stack both intents (b53bea8). The pre-existing reset-door pins and the conformance pin are byte-identical to main.",
"premise_reading": "Unchanged since patch round 1: no stored value persists a served token. This round adds no write path; the merge brings none that stores a version token.",
"superseded_pins": [
{
"file": "packages/cli/src/commands/migrate/preview-read-only.integration.test.ts (added by #21432)",
"title_before": "audit-metadata-bodies without --apply on a database that does not exist exits 1 with both tables counted unread",
"title_after": "audit-metadata-bodies without --apply on a database that does not exist exits 1 with every audited table counted unread",
"assertions_before": "exitCode 1; payload.apply false; report.failures toBe(2); report.scanned toBe(0); sorted byObject keys toEqual([sys_activity, sys_audit_log])",
"assertions_after": "audited = [sys_activity, sys_audit_log, sys_metadata_audit]; exitCode 1; payload.apply false; report.failures toBe(audited.length) (3); report.scanned toBe(0); sorted byObject keys toEqual(audited)",
"why": "Stacked intents. #21391: every audited table is counted unread. #21207: the audit set gains the decision-audit trail. No assertion was dropped or loosened. The #21391 comment above the case is kept, and a #21207 note is added at the case.",
"derivation": "The set is literal. The plugin exports no combined list of what the run reads: STORED_METADATA_BODY_AUDIT_OBJECTS covers only the two copy tables, METADATA_DECISION_AUDIT_OBJECT is not exported from the package index, and the runner composes its table list inline. A literal set fails loudly on any later widening; it cannot desync silently."
}
],
"control": "Control C1 via scripts/ablation-replace.mjs --delete, anchored on the decision-audit entry of the runner's table list in packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts. The anchor hit once (x1 to x0) and the blob moved 386a02f5484a to 62a90d7c9ff0. The CLI resolves plugin-audit to source through its vitest alias. The updated pin went red, 1 of 9: expected 2 to be 3, from the failures assertion. Restore: blob == HEAD (386a02f5484a), git diff HEAD empty, status clean.",
"tests": [
"Full build at the merge commit, before the test-only commit: turbo run build --filter=!@objectstack/docs --concurrency=2 under os-verify-lock, VERDICT command-exit 0 (6m30s on a shared box).",
"typecheck, each exit 0 through os-verify-lock: metadata-protocol (tsc --noEmit); objectql (tsc --noEmit, then scripts, then check:test-typecheck); plugin-audit (same three); cli (tsc --noEmit, then check:test-typecheck).",
"Suites at b53bea8, each through os-verify-lock with VERDICT exit 0. Commands are pnpm --filter PKG exec vitest run --maxWorkers=2 unless noted:",
"cli --project integration (queue population): 76 files, 687 passed, 2 skipped",
"cli --project unit: 248 files, 3552 passed",
"client: 50 files, 644 passed",
"metadata-protocol: 204 files passed, 3 skipped; 3077 passed, 19 skipped",
"plugin-audit: 38 files, 618 passed",
"The three named CLI files run explicitly with --reporter=verbose: preview-read-only.integration.test.ts 8 passed, 1 skipped; schema-migrate.one-shot-family.integration.test.ts 44 passed; delete-reset-carriers.test.ts 20 passed. Total 72 passed, 1 skipped.",
"Byte identity at HEAD, base 41a3c8d and origin/main alike: client meta-delete-item-carriers.test.ts 7950f3f1d6d3, cli delete-reset-carriers.test.ts b6f9c518f0ff, objectql publish-package-drafts-response-conformance.test.ts 63f687725a1d."
],
"gates": "At b53bea8. dispatch-gates --repo objectstack-ai/objectstack --commands derived 84 commands; all 84 ran and recorded exit 0. --ran: 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. pnpm lint on the whole repo: exit 0, 135s on a shared box. check:error-code-casing: exit 0.",
"mcp_calls": "0",
"api_writes": "1: POST /repos//issues/21207/comments (this os-dev-report) through the fleet-write relay. git push 7660d81..b53bea8, a fast-forward (7660d81 is an ancestor, exit 0); not a REST write. Reads: REST GET of the check-runs on b53bea8, once.",
"checks_after_push": "Read once at b53bea8, right after the push: 0 check runs registered yet. Not polled; CI convergence is the seat's.",
"open_questions": [],
"deviations": [
"The PR body is not edited; the text for the seat is in pr_body_edits_for_seat.",
"origin/main moved one more commit (086ad0a) after the merge. dispatch-gates reports that none of what it derives from was touched. No second merge was made.",
"No other file changed this round besides the merge and the one pin."
],
"files_changed_this_round": [
"merge commit 8ca4966 (origin/main 53fd35e into the branch, automatic)",
"packages/cli/src/commands/migrate/preview-read-only.integration.test.ts (one case: title, a #21207 note, the literal audited set)"
],
"pr_body_edits_for_seat": "In "Superseded pins updated", add a bullet: - The absent-database audit pin that #21432 added (dry run of the audit-metadata-bodies migration on a database that does not exist) counted two tables unread. The audit now also reads the decision-audit trail, so it counts every audited table: three, each named, none scanned, exit 1. A control that removes the decision-audit table from the run turns it red. In "Acceptance notes", "Serial constraint" bullet: also say origin/main was merged again (8ca4966, including #21432) and the pin above was stacked.",
"out_of_scope_findings": []
}
objectstack-fleet commentedon Oct 2, 2026
ACCEPT: PR #21436 at
b53bea8da5(exit two of the stored-metadata-body family: every door that serves a stored content hash serves it keyed, every door that takes a version token back compares it keyed, and copies carry no hash).Fixes #21207; landing through the queuedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T20:22Zb53bea8da5, by an isolatedCONTRACT_REVIEW_TIERagent. The record is comment5960845421on the PR.7660d811a7came from that head's red CI and was held, not posted. Every judgment that did not depend on the merge carried over.1ad5a0099e: option A was falsified. With no crypto provider, the receipts served an empty version token. The SDK and the CLI drop an empty pin, so the pre-existing reset-door pins (the SDK's and the CLI's) failed open. The fix is the first patch round.7660d811a7: a relay with PR fix(cli,runtime): one-shot CLI boots run no seed loader and arm no lifecycle sweep; every no-write mode boots read-only #21432. fix(cli,runtime): one-shot CLI boots run no seed loader and arm no lifecycle sweep; every no-write mode boots read-only #21432 ([finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391) landed an absent-database audit pin written for two tables, and this PR widens the audit to the decision-audit trail.meta.deleteItemsends none of the three carriers the REST reset door reads — noIf-Match, no?state=draft, no?dropStorage, on either declaration #12181,os meta deletecannot pin a reset or discard only the pending draft — the CLI stays one layer behind the SDK it calls #13024), and fails open.409 METADATA_CONFLICT. A held token costs one 409 across a restart, or when a provider is first registered.Clause-②: yes (narrowing). Six packages are bumpedminorwith the BREAKING banner, and ADR-0087 isnot-required (no-migration-prescription). This is the family's precedent level.@objectstack/restchanges a comment only and carries no changeset.os servealways registers a provider, and the cost is a loud 409, never a lost update. A key derived from a configured secret becomes a follow-up only if such a deployment is real.metadata-protocol. It is the narrower change, and it fails closed for the MCP reader.b53bea8da5, collapsed latest-per-name: 34 names, 29success, 5 skipped, 0 red.check-governed-merges --pr 21436reads not governed (22 paths, 2903 changed lines), andmergeable_statereadsclean.meta-delete-item-carriers.test.ts,delete-reset-carriers.test.ts) and the objectql conformance pin. The only earlier pin edited is the stacked fix(cli,runtime): one-shot CLI boots run no seed loader and arm no lifecycle sweep; every no-write mode boots read-only #21432 audit pin. Within this PR's own family, the two decision-note pins are inverted, as fork three A requires.finding, under the data-leak exception, reach not measured).aggregations(no member serves the value). Acceptance notes: a wording gap, not a hole.@objectstack/clientand the CLI integration tier, because no source changed there. That was the seat's dispatch-order miss: both drive the real protocol. It is recorded on the seat post, not as a card.pr_readyandautomerge_enablefollow in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Landed: PR #21436 →
713b0fa76d(exit two of the stored-metadata-body family: keyed served hashes and keyed inbound tokens, with no hash in any copy).Fixes #21207: the card is completedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T20:52Z713b0fa76dis onorigin/mainas a single-parent squash: 22 files, +2765 / −138. Itsgit patch-id --stableis11c67d4b177d…, equal to the reviewed net diff atb53bea8da5.3ddd3d0c4a;pm:dispatchedis removed in the same act as this note.Generated by Claude Code
fix(mcp)!: the MCP stdio engine-only reader joins the stored-metadata…
feat(spec): ICryptoProvider gains a required keyedDigest member; Loca…
feat(automation): a flow's credentials live in a write-only channel o…
fix(metadata-protocol)!: a metadata body's stored content hash is ser…
fix(metadata-protocol)!: one stored-metadata filter collector and sea…