Skip to content

[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

Closed

Description

@objectstack-fleet

Ruled: 5950183039 · letter A / A / A · 2026-10-02T10:24Z

Ruled: 5942670275 · letter B · 2026-10-01T23:30Z

立卡门 ①(例外:可能泄露数据,已测可达)。finding 类别 a。reach: 公开入口实测一次(管理员 API key,真实启动)。
动手的读者:分诊定级和定车道后,由承接该族的车道席位派发。族的上一张收口卡 #21120 在 domain:services;第一处出口的代码在 packages/mcp(domain:cli)。
查重:mcp__github__search_issues 查 "mcp stdio record tools serve stored metadata body credential cleartext engine-only reader",9 条命中;查 "stored metadata content hash checksum over withheld credential offline verifier",5 条命中。都含 closed,都不是本题。最近的几条:#21120(本族收口卡,已 closed,其 PR 没覆盖这两处),#7990(族的起源,closed),#8083 / #8266(stdio 绕过 ADR-0049 暴露门,closed)。
⚠️ 安全类卡:只写类别与位置,不写请求、字段路径或值。detail 留在 dev 的私有暂存区。

来源

#20790 的 dev 实测报告 5935832144(out-of-scope findings 第 1、2 条)。实测在 main 的 cb45469e 上做,已包含 #21120 的收口 PR(336e1914)。

出口一:engine-only 读取门(类别 a)

  • 位置: packages/mcp/src/stdio-data-bridge.ts 的 query 与 get 路径(约 :353、:146),以及同包 plugin.ts 的 record resource(约 :597)。三处都只经引擎读取。
  • 实测: 用真实的管理员 API key,按 stdio 门自己的解析方式调用,门返回存储行与版本历史行的元数据体,其中的凭据为明文。成员调用被拒(PERMISSION_DENIED)。
  • 原因(源码读): 族的投影只挂在协议的数据门方法上。本族的枚举 pin(stored-metadata-body-family.pin.test.ts)覆盖了数据门、audit、analytics、realtime,没有覆盖这个门。
  • 范围: 实测用的是流程元数据体。数据源元数据体走同一条引擎读,从构造上同样受影响(源码读,未单独实测)。

出口二:内容哈希(类别 a,安全例外)

  • 位置: packages/metadata-core(或其仓库层)sys-metadata-repository.ts 约 :537:存储行的内容哈希是对整份存储体做 sha256,凭据也在其中。
  • 实测: 通用数据门向管理员提供这个哈希,且它等于保存回执里的版本号。
  • 后果: 对低熵凭据而言,哈希加上已投影的正文,就构成一个离线校验器,可以验证对凭据的猜测。

与 #20790 的关系

#20790(流程凭据进只写通道)落地后,流程的凭据会离开存储体,上面两处出口对流程就关闭了。对数据源不会关闭。所以本卡不依赖 #20790,但定级时可以只按数据源和历史存量来量剩余范围。

方向(供分诊参考,不是裁决)

  • 出口一:按族的既有做法,把这个门纳入投影或拒收,并加进族的枚举 pin。⛔ 不新造第二套投影。
  • 出口二:哈希只对投影后的正文计算,或不再对外提供。哪种做法会影响版本号语义,由分诊判断。

Generated by Claude Code · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

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

    1. 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.
    2. 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.


    Generated by Claude Code

  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    priority:p1High: required for production / M2
    on Oct 1, 2026
  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 of the domain:cli seat's session session_01VvcEokUG1tvVxkceYfR5XB (batch 3): priority:p1 security, 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).
    • 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.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    待维护者裁决:出口二(对外提供的内容哈希)要加密钥,但仓库里没有可用的密钥来源

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-01T19:02Z

    状态安排: 出口一由 PR #21228 交付(Part of #21207),正在复核与 CI 中,与本裁决无关、照常落地。它合并的同一笔里,本卡从 pm:dispatched 转 needs-user-decision,退出活动队列,进决策箱。在那之前本卡仍在飞。

    一句话问题: 管理员读取元数据存储行时,每行都附带一个"版本号"。它是对整个存储体(含被隐去的凭据材料)直接算的 sha256。拿到它的人可以离线逐个猜凭据、对答案,猜中即知。分诊裁定改成"加密钥的哈希"(HMAC),但仓库里没有任何现成密钥能送到算哈希的地方。dev 按认领的停止条款停手,未写出口二的代码。

    背景

    • 分诊裁决 5936231568 第 2 条:对外提供的内容哈希改为 HMAC(服务端持有的密钥),⛔ 不缩窄成"只对投影体算哈希"。缩窄会让只改凭据的保存不再推动版本号,版本身份就坏了。
    • 认领 5937050935 的停止条款:若任一消费方跨部署比较哈希,或密钥来源需要新的秘密,就先停、先报。后一条触发了。
    • dev 报告 5938425782:
      • 列出了全部哈希消费方。在本检出内没有跨部署比较;cloud 仓未读,记 NOT MEASURED。
      • 实测了密钥来源:加密提供者契约只有 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)」。即契约允许不加密钥,所以不能依赖它来提供本安全性质。
    • 协议声明:选项 B 要给加密提供者契约加一个方法,是公开契约放宽(Clause-②: yes),走 spec 车道并需契约复审;选项 A 要修改上面的不变量 4。

    前提(各带复查命令)

    1. 加密提供者契约里没有带密钥的方法。git grep -nE "^\s*(digest|encrypt|decrypt|rotateKey)\b|hmac" origin/main -- packages/spec/src/contracts/crypto-provider.ts:今天只命中四个方法。
    2. 本地提供者的 digest 不加密钥。git grep -n "createHash('sha256')" origin/main -- packages/services/service-settings/src/local-crypto-provider.ts::501。
    3. 存储行哈希列宽 71 字符,历史表 80 字符;带前缀的密钥哈希需要 76 字符。git grep -n "maxLength: 71" origin/main -- packages/metadata-core/src/objects/sys-metadata.object.ts。
    4. 真正带凭据材料的,主要是已有存量行及其历史。dev 实测:数据源保存入口今天会拒收带凭据的存储体(422),流程存储体已由 security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790 拿走凭据。复查 = 本卡 dev 报告的 repro.exit_two 与 key_source 段。
    5. 只有管理员角色能拿到这个哈希,成员角色全路径被拒。复查同上 repro.exit_one。

    选项 × 真实代价

    选项 做什么 客户可感知的后果
    A 写入时加密钥 新增一个专用服务端秘密(生产缺失即拒启动),新写入的行存 HMAC。另做一次迁移,重算所有存量行与历史行的哈希。 每个部署多配一个秘密,运维可见。要改仓库契约不变量 4、合同测试套件、两个哈希生产方、列宽和 spec 描述,还要改写只增不删的历史表。迁移切换后,客户端手里的旧版本号会各撞一次 409。
    B 对外提供时加密钥 存储照旧是规范 sha256(契约、两个生产方、文件层、父链全不动)。只在对外提供哈希的每个出口,把它换成"HMAC(密钥, 存储哈希)";入站版本号也按同形比较。密钥来自已有的 OS_SECRET_KEY,经加密提供者契约新增的带密钥方法取得。没挂提供者时不提供哈希,绝不提供原值。 不加新的运维秘密。要给 spec 契约加一个方法(Clause-②: yes),所有提供者都要实现。要枚举并钉住"提供哈希的出口族":数据入口两张表、本 MCP 入口、保存回执、/meta 版本与历史、HTTP 缓存校验值。切换后旧版本号各撞一次 409。
    C 重裁:不加密钥 只对"投影隐去了材料"的行不提供哈希,其它行照旧提供。 那些行失去版本号,乐观锁退回"后写者赢"。这恰好是带凭据的那批行。需要推翻分诊"加密钥、不缩窄"的裁决。

    业务含义直译

    • A = 给每张存档单据换一种防伪编号,并且把档案室里的旧编号全部重印一遍。
    • B = 档案内部照旧编号,对外开出的每张收据都盖一层只有本店认得的钢印。
    • C = 对装了钥匙的那几个档案袋干脆不开收据。

    四轴(业务立场)

    • ① 项目长远合理性: B 把"带密钥"放在对外提供的那一层,和存储体投影是同一类边界,仓库内部的规范哈希不变量原样保留。密钥归现有的秘密持有者,将来可换 KMS。A 改的是仓库契约本身,两个生产方都要接入新秘密,历史还要重写。C 是又一种缩窄,版本身份在最敏感的那批行上失效。
    • ② 实际业务拉动: 今天撞上的是持有管理员密钥的人,他能拿到存量带凭据行的哈希做离线猜测(实测)。真正处于风险中的是存量与历史。B 在每个出口上一次覆盖,不重写历史;A 不做迁移就几乎保护不到实测中的风险面,做迁移才能覆盖。
    • ③ 防 AI 犯错: B 是一个提供时函数加一个钉住的出口族,和本家族存储体投影已证明可行的模式相同;新出口不进清单就让钉子测试变红,没挂提供者就不提供哈希(失败关闭,响亮)。A 让一个新秘密贯穿两个生产方,哪一处漏接就静默地写回不加密钥的值。
    • ④ 创业阶段不扩散: B 多一个契约方法,不多运维秘密。A 多一个秘密、一次迁移和多处契约修订。C 什么都不加,但要重开裁决,并在带凭据的行上降级锁。

    os-decision-facets

    • ① 长远:B 不扩大特例,仓库契约的规范哈希不变量原样保留;A 修订仓库契约并重写历史;C 新增一类"无版本号"的行。
    • ② 拉动:实测拉动存在(管理员可离线猜存量凭据);风险面是存量与历史,B 一次覆盖,A 须迁移才覆盖。
    • ③ 防错:B = 单一提供时函数 + 闭合出口清单 + 失败关闭;A 漏接处静默写回不加密钥的值。
    • ④ 不扩散:B 只加一个契约方法;A 加秘密、迁移与契约修订;C 不加但重开裁决。
      Prior rulings read: HMAC|keyed hash in docs/adr → 4 hits (all webhook / notification signing, unrelated); ADR none; thread: 5936231568 (triage ruling 2)

    推荐:B。回退:C,作为过渡,仅当此刻不愿放宽 spec 契约时选。

    • 只看①选 B;②③④ 是否翻转:否。
    • 终态参照:两年后的平台应把对外版本号当作不透明令牌。RFC 9110 把 entity-tag 定义为不透明的校验值;Kubernetes 的 resourceVersion 也是不透明令牌,不是内容摘要。B 正是这个形态。

    置信缺口:

    • cloud 仓(环境提升、市场)有没有跨部署比较这个哈希,本席看不到(NOT MEASURED)。若有,B 的提供时密钥会让跨部署比较失效,需要该处改比较存储哈希。
    • 也可以选择把现有 digest 改为带密钥,而不加新方法;但契约允许 digest 不加密钥,依赖它就是"声明而未强制",本席不荐。

    裁后执行

    • 选 B:
      • 先在 spec 车道立一张 Clause-②: yes 卡,给加密提供者契约加带密钥的方法,并经契约复审。
      • 再把出口二按"提供哈希的出口族"枚举、钉住、派发(本卡或其衍生子卡),本卡至此关闭。
      • 出口一已落,不受影响。
    • 选 A: 立"新秘密 + 历史重算迁移"卡,并在发布说明里写明运维须配新秘密,其余同上。
    • 选 C: 由分诊重裁并改本卡验收条款,再派发。

    你要做的(一个动作): 回复 A、B 或 C(可附条件)。


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-01T19:13Z

    • 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-report 5938425782, 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:
      • the projection removed: 5 red;
      • the refusal disabled: 9 red;
      • an extra engine read: 1 red;
      • an unclassified member: 1 red.
    • Changeset, sentence by sentence against the diff:
    • 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.

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-01T19:45Z

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

    Generated by Claude Code

  8. 35 remaining items

  9. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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."
    }

  10. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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."
    ]
    }

  11. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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": []
    }

  12. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-02T20:22Z


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-02T20:52Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:clipriority:p1High: required for production / M2security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions