Skip to content

fix(metadata-protocol)!: a datasource restored or published from a stored body may name only the credentials reference its row holds now - #27860

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-rkqt9u
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-rkqt9u

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #27818

domain:server#3 · os-elon-musk · session_016WV4d1qRCNJhpDgVXFoXkp (card thread)

Clause-②: no (narrowing) — the restore doors and the draft publish refuse a stored datasource body whose external.credentialsRef is not the reference the active row they write into holds now, where they wrote it back as stored before. Nothing is widened.
Metadata-protocol: narrowed
Skills-docs: none — skills/** and content/docs/** teach external.credentialsRef only on datasources declared in code, as an opaque handle the secret store mints; none teaches restoring or publishing a stored datasource version or draft that names one, and the refusal itself names the door that binds a credential.

维护者速读

  • 改了什么:把已存的数据源定义重新变成生效版本的三个入口(按历史版本回滚 POST /api/v1/meta/datasource/:name/rollback、按提交还原 POST /api/v1/packages/:id/commits/:commitId/revert 的恢复分支、草稿发布 POST /api/v1/meta/datasource/:name/publish 及整包发布),现在和 fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817 的保存入口用同一条规则:写回的 external.credentialsRef 只能是它要写入的那条生效记录现在持有的引用;不是就 400 拒绝,什么都不写,无论引用指向什么答复都相同,消息里不出现引用值。
  • 为什么改:卡上第 1 项要求实测恢复入口。实测三个入口都会把历史版本或草稿里存着的引用原样变成生效值,包括别的数据源的引用、无人持有的引用、本数据源轮换前的旧引用,不符合 objectql: a secret field accepts an existing secret: ref from a non-system write, so it can name another record's credential row #27582 的规则。第 2 项(修复前已存的数据)的只读审计已作为卡上评论发出。
  • 风险与回滚:拒绝发生在任何写入之前;写回的版本或草稿若带着现在这条记录持有的同一个引用,照常通过;不带这个键的照常写回(不绑定凭据,和保存时省略该键一样)。会被拒的两种正常情形:回滚或还原到一个引用已被轮换掉的旧版本,以及引用轮换后再发布之前存下的草稿;补救是省略该键重新保存,或用数据源管理入口重新绑定凭据。回滚只需 revert 本 PR。
  • 先看:packages/metadata-protocol/src/protocol-datasource-credentials-ref.ts(判定本身,新增入口措辞),packages/metadata-protocol/src/protocol-authoring-gate.ts 中 refuseRestoredBodyTheSaveDoorRefuses 的一处调用,packages/metadata-protocol/src/protocol.ts 中 promoteDraftForPublish 的一处调用。
  • 两分钟验证:pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.datasource-credentials-ref-restore-doors.test.ts,预期 21/21 通过。
  • 需要你做的:无,席位落地;本 PR 是收窄(narrowing),落地前需一次契约复审,由席位安排。

What changes

The rule is the one #27582 set and #27762 (PR #27817) applied to the metadata save door: a non-system write may only seal a new value or keep its own record's stored reference. The doors that make a stored body the active row seal nothing, so the one reference the body they write may name is the one the active row they write into holds now.

  • The judge is fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817's, not a copy. datasourceCredentialsRefSaveRefusal gains an optional door: 'restore' | 'publish', which changes only the refusal's words. The save door's words are byte-identical. Each new door asks it as an active write (mode: 'publish') into the row it lands in, with no source, so the comparison is with that row's current reference.
  • The restore doors. rollbackMetaItem and revertCommit's restore limb both reach the restore path's save-door refusal family, refuseRestoredBodyTheSaveDoorRefuses in protocol-authoring-gate.ts. The judge is asked there, after unknownDatasourceSaveRefusal, in the save door's order, against binding, the package binding of the active row the version is restored into. That family runs inside restoreVersion before the active row is read and before the put, so a refused version writes nothing. protocol.ts gains one host line (getOverlayRepo) and one docblock bullet.
  • The draft publish. promoteDraftForPublish, shared by the per-item publish and the package publish, re-asks the save door's refusal family on the draft body. The judge is asked there after unknownDatasourceSaveRefusal, against promoteInto, the binding the active row is written under, before repo.promoteDraft. One block in protocol.ts; nothing in it moves.
  • Who passes. No server-stated restore or publish exists: the restore doors are reached only from the REST rollback route and the runtime package revert and rollback routes, so none passes through. The save door's own rule and its source pass-through are unchanged.
  • Unchanged. A version or draft naming the reference the row holds now is written and keeps it. One naming no reference is written as it stands, with no credential bound, as a save omitting the key is. Rows already stored are not re-judged at rest.

Why

The card's item 1, measured on origin/main fec60e0 with #27817's head (4258efa) merged in: each of the three doors wrote a stored datasource body as the active row with whatever external.credentialsRef it held. The pin below was committed first and was red on that tree: 15 failed | 6 passed (21), every refusal case at every door, with the six accepted cases green.

Item 2, the read-only audit of rows stored before the rule, is on the card (comment 6512713420): what os secret orphans covers, what it misses, and the operator's reading. It proposes no migration and no repair.

Tests

  • packages/metadata-protocol/src/protocol.datasource-credentials-ref-restore-doors.test.ts (new, 21 cases): the real rollbackMetaItem, revertCommit and publishMetaItem over a stub engine that stores rows and history, the double of protocol.restore-doors-save-refusal-family.test.ts.
    • Refused at each door, with code and status asserted, the store byte-identical afterwards and the row's reference kept: a body naming a reference another datasource holds, one no row holds, the reference its row held before and holds no longer, and one where its row holds none now. The four answers are identical, each names the datasource and the key, and none contains any reference. revertCommit reports it in failed[] with code: 'VALIDATION_ERROR'.
    • Accepted at each door: a body naming the reference its row holds now keeps it; a body naming none stores none.
  • packages/qa/dogfood/test/datasource-credentials-ref-restore-doors.dogfood.test.ts (new, 4 cases): the same rule at POST /api/v1/meta/datasource/:name/rollback and /publish of a booted stack, as a signed-in non-system caller — a refused rollback and a refused publish, each with the active row unchanged, and one accepted case at each door.
  • scripts/engine-double-contract.pinned.json: the unit pin's stub engine recorded (--write; 3 rows added, none lost).

Acceptance notes

  • The draft publish is in scope. The card names the restore doors; the dispatch order adds "any other restore path that writes a datasource row without passing saveMetaItem's judge". The repository has three write sites from the protocol: the save, the draft promotion and the version restore. The draft promotion is the one left, so it is judged too. The seat agreed before the fix.
  • A refused publish after a rotation. A draft saved while its row held one reference cannot be published once the row's reference has moved. Saving the draft again with a different reference is refused too, because the save door judges a draft against its own draft row. The remedy the refusal names is to save the draft with the key omitted, or discard it, and to bind the credential through the datasource admin door.
  • The package rollback. rollbackToPackageCommit runs revertCommit per commit and counts a commit as reverted when it does not throw. A refused item lands in that inner revert's failed[], which the package rollback does not surface. That is how every per-item refusal of the restore limb already behaves; it is not changed here.
  • protocol.ts gains one host line, one docblock bullet and one call-site block; nothing in it moves or reflows (serial queue [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467). The restore call site is in protocol-authoring-gate.ts, the restore helper beside it.
  • scripts/engine-double-contract.pinned.json is outside the declared file surface: the gate requires the new pin's engine double in the ledger.
  • Declarations: Metadata-protocol: narrowed (three doors refuse a body they wrote), hence a minor bump on @objectstack/metadata-protocol and the ! title. ADR-0087: not-required (no-migration-prescription) — the refusal prescribes a remedy for the request, not a migration of stored data.

Verification

All at the branch head unless stated. Heavy runs under scripts/pm/os-verify-lock.sh.

  • pnpm --filter @objectstack/metadata-protocol typecheck exit 0; vitest run over the whole package at fd02314: 282 passed | 4 skipped (286) files, 21842 passed | 26 skipped (21868) tests, exit 0.
  • pnpm exec turbo run build --concurrency=2 --filter=!@objectstack/docs at fd02314: 114/114 tasks, exit 0.
  • node scripts/pm/dispatch-gates.mjs --ran at fd02314: 73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. check-plugin-teardown-shape --self-test first exited 3 (its fixture commit is outside the shallow clone); after fetching that commit it exited 0. pnpm lint exit 0.
  • Unit ablations of the call sites, each through scripts/ablation-replace.mjs (mutate, run the pin, restore; the restore proven blob-equal to HEAD with git diff HEAD empty):
    • the restore call site never throws → 10 failed | 11 passed (every refusal at both restore doors);
    • the restore call states a source, taking the pass-through → 10 failed | 11 passed;
    • the publish call site never throws → 5 failed | 16 passed (every refusal at the draft publish);
    • the publish judges against the draft row instead of the active row it writes into → 5 failed | 16 passed.
  • Dist ablation at the REST doors: both call sites disabled, @objectstack/metadata-protocol rebuilt, scripts/ablation-dist-preflight.mjs proved both markers in dist/ (exit 0), the dogfood pin 2 failed | 2 passed (4) (the two refusals); both files restored (blob equal to HEAD), rebuilt, preflight --absent exit 0 for both markers with the tree clean, the pin 4 passed (4).
  • The save door's words are unchanged: the message the judge builds with no door was compared with fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817's at 4258efa, byte for byte (equal).
  • Dogfood battery (pnpm --filter @objectstack/dogfood test, the whole battery, at fd02314): 354 passed | 3 failed | 3 skipped (360) files, 2871 passed | 4 failed | 20 skipped (2895) tests. The four failures are not this change's; they are red on main's code too:

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: no.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief excepted).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4d1qRCNJhpDgVXFoXkp

…s credentialsRef at the restore doors and the draft publish

The doors that make a stored body the active row (rollbackMetaItem,
revertCommit's restore limb, publishMetaItem) may bring back only the
credentials reference the row they write into holds now. Red on this tree:
each of the three doors writes every refused case (15 failed | 6 passed).

Claude-Session: https://claude.ai/code/session_016WV4d1qRCNJhpDgVXFoXkp
Co-authored-by: Claude <noreply@anthropic.com>
…ored body may name only the credentials reference its row holds now

The own-reference rule the metadata save door applies to a datasource's
external.credentialsRef now also holds at the doors that make a stored body
the active row: the restore doors (rollbackMetaItem and revertCommit's
restore limb, through the restore path's save-door refusal family) and the
draft publish (the per-item publish and the package publish). Each asks the
save door's judge as an active write into the row it lands in, and is
refused VALIDATION_ERROR / 400 before anything is written, with one answer
whatever the reference names. A body naming no reference is written as it
stands. The judge gains a door word for its restore and publish wording;
the save wording is unchanged.

Claude-Session: https://claude.ai/code/session_016WV4d1qRCNJhpDgVXFoXkp
Co-authored-by: Claude <noreply@anthropic.com>
…rollback and publish doors; changeset

The rule holds at POST /api/v1/meta/datasource/:name/rollback and
/publish of a booted stack, as a signed-in non-system caller. The unit pin's
engine double is recorded in the engine-double ledger (--write: 3 rows).

Claude-Session: https://claude.ai/code/session_016WV4d1qRCNJhpDgVXFoXkp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 16 documentable anchor(s).

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef.

⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef → packageMentionDocs.

Which tree this was computed on

This run read content/docs from deb0226e20bbe36cc1467b064faa2b125befa656 — the merge of head fd02314b73a44b821cc9a3ccbf27f43114e3ac56 into base c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin deb0226e20bbe36cc1467b064faa2b125befa656 && git checkout deb0226e20bbe36cc1467b064faa2b125befa656
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef fd02314b73a44b821cc9a3ccbf27f43114e3ac56 && git checkout -B drift-repro c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef && git merge --no-ff fd02314b73a44b821cc9a3ccbf27f43114e3ac56

node scripts/docs-audit/affected-docs.mjs --json c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head fd02314b73a44b821cc9a3ccbf27f43114e3ac56 · 2026-10-30T02:20Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: no (narrowing), Metadata-protocol: narrowed).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fd02314b73a44b821cc9a3ccbf27f43114e3ac56
Local-runs: probe — a node one-liner comparing the save-door message literal on main (7b8bcfe) with the one the head builds with no door, to answer the byte-identity question (equal); nothing of the tree was built, run or re-run (record-recognisers.mjs --template was printed for the record's shape only)
Scope: packages/metadata-protocol/src/protocol-authoring-gate.ts, packages/metadata-protocol/src/protocol-datasource-credentials-ref.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol.datasource-credentials-ref-restore-doors.test.ts, packages/qa/dogfood/test/datasource-credentials-ref-restore-doors.dogfood.test.ts, scripts/engine-double-contract.pinned.json + packages/metadata-protocol/src/sys-metadata-repository.ts (restoreVersion, promoteDraft, headAt, resolveWriteHead, whereFor), packages/metadata-protocol/src/protocol-stored-row-bindings.ts (resolveOverlayPackageBinding), PR #27817's record (comment 6512185647: the judge's address, order and pass-through, reused where unchanged). The changeset .changeset/27818-datasource-restore-publish-credentials-ref.md is a prose face: the lane seat's ACCEPT.

① Derived judgments

  • The rule. objectql: a secret field accepts an existing secret: ref from a non-system write, so it can name another record's credential row #27582's, as fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817 applied it at the metadata save door: a non-system write may only seal a new value or keep its own record's stored reference. The doors that make a stored body the active datasource row seal nothing, so each has an accept set of one value for external.credentialsRef: the one the active row it writes into holds now. Extending the same judge to those doors is the rule, not a new one.
  • Accept set, before → after, per door:
    • rollbackMetaItem (REST POST /api/v1/meta/datasource/:name/rollback) and revertCommit's restore limb (POST /api/v1/packages/:id/commits/:commitId/revert, and through it rollbackToPackageCommit): before, a history version was written back with whatever reference it stored; now a version naming a reference the active row does not hold now is refused VALIDATION_ERROR / 400. Correct.
    • promoteDraftForPublish (the per-item POST /api/v1/meta/datasource/:name/publish and the package publish batch, both reach it): before, a draft went live with whatever reference it stored; now a draft naming a reference the active row at promoteInto does not hold is refused the same way, before repo.promoteDraft. Correct.
    • Still accepted at every door: a body naming the reference its row holds now (kept); a body naming none (stored with none, as a save omitting the key is — the same reading fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817 accepted). Both pinned at each door.
  • Q1 — the save door's words are byte-identical to fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817's: the head's three-way message builds the no-door string from the same fragments, and the two literals compare equal (the probe above). door changes only the words: the judged address, the comparison and the pass-through are the lines fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817 reviewed, unchanged. One answer per door whatever the reference names: each door's message names the datasource and the key only; the unit pin asserts the four refusal answers at each door are equal and that none contains any reference; the dogfood pin asserts code and status at the REST doors as a signed-in non-system caller. Holds.
  • Q2 — each call reads the row its write lands in.
    • Restore: restoredBodyWriter is handed binding = resolveOverlayPackageBinding(type, name, member), the package_id of the active row found by the package-agnostic (type, name, state: 'active', member) lookup; restoreVersion runs deriveRestoredBody first, then finds the active row with the same package-agnostic lookup and puts with that row's package_id. The judge reads headAt(active, packageId: binding, member), an exact package_id match (string → eq, null → IS NULL), which is the address the put resolves to (resolveWriteHead with state: 'active' does no fallback). Same row.
    • Publish: promoteDraft writes the active row at draftRow.package_id ?? adoptInto ?? null; with packageId: draftKey and adoptInto: promoteInto (only when draftKey === null), that is promoteInto in every branch. The judge reads headAt(active, packageId: promoteInto). Same row, and the same key the other publish-side save-door judges in that block already read.
    • source: undefined at both call sites, so neither takes the server-stated pass-through; no restore or publish caller states a source (read tree-wide: repo.restoreVersion and repo.promoteDraft have exactly the three callers in protocol.ts).
  • Q3 — placement and coverage. The restore judge sits in refuseRestoredBodyTheSaveDoorRefuses, after unknownDatasourceSaveRefusal and before the elevated-flow and runtime-authoring checks, in the save door's order; it runs inside restoreVersion before the active row is read and before the put. The publish judge sits after unknownDatasourceSaveRefusal and before repo.promoteDraft. Nothing is written on either path before the refusal (the unit pin snapshots the store before and after each refusal, equal; the dogfood pin reads the active row back unchanged). Every path that turns a stored body into the active datasource row from the protocol is judged: rollbackMetaItem, revertCommit's restore limb, rollbackToPackageCommit (runs revertCommit per commit), the per-item publish and publishPackageDrafts (both through promoteDraftForPublish). The remaining stored-row writer is migrateStoredMetadata (source: 'migrate-stored'), the server-stated rewrite of a row's own body that fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817 accepted as the pass-through; a package install writes from a manifest, not a stored body, and the package duplicate and unmanaged import go through saveMetaItem, judged since fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817. No reactivation door exists.
  • Package publish batch: one promoteDraftForPublish throw is re-thrown with __batchItem out of the loop, inside inTxn; with an engine transaction() the batch rolls back whole. That is how every per-item refusal in the batch already behaves; nothing is changed here.
  • Published surface: no export, type or schema moves; datasourceCredentialsRefSaveRefusal is unexported and gains an optional parameter. DatasourceSchema untouched.
  • Q5 — the ledger. The new unit pin's stub binds delete, findOne and update to the engine-double predicates: three rows, one per bound verb (15 lines), none lost; Lint · ESLint, which carries the Engine test-double contract gate, is green at the head.

② Semver level

  • @objectstack/metadata-protocol: minor, with Clause-②: no (narrowing) and Metadata-protocol: narrowed line-initial on the PR body and the changeset, the BREAKING banner, the ! title and one ADR-0087 marker. narrowed is the kind (three doors refuse a value they wrote before); a kind other than none takes at least minor (AGENTS.md Post-Task Checklist step 3). Consistent, and the same level and kind as fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference #27817.
  • ADR-0087 not-required (no-migration-prescription): no key, export or type is added, removed or re-shaped; the changeset carries a remedy for the request (restore a version naming the served reference, re-save with the key omitted, bind through the admin door), not a prescription for rewriting stored data. Check Changeset is green at the head. Correct.
  • Skills-docs: none — REASON present on the PR body (lane seat's ACCEPT to confirm the sentence).

③ Boundary flags

  • Q4 — the two stated side effects are acceptable. A rollback or revert to a version whose reference the row no longer holds (rotated since, or stored before the save door judged the key), and a publish of a draft stored before a rotation, are now refused. Both are the rule's own consequence (a reference an active row does not hold now is not that row's to carry forward), both are stated in the changeset under Who sees it, and each carries a remedy that keeps the capability reachable through the door that mints a reference. No silent loss: the refusal names the door.
  • Package rollback reporting (pre-existing, stated in the PR). rollbackToPackageCommit counts a commit as reverted when revertCommit does not throw; a per-item refusal, this one included, lands in the inner failed[], which the package rollback does not surface. The shape is every per-item refusal's on that path and is not changed here. The seat may file a report-only follow-up (surface the inner failed[]), outside this card.
  • Batch publish on an engine without transaction() (the memory driver): items promoted before a refused one stay promoted. Pre-existing for every refusal in the batch; not this diff's.
  • Q6 — Fixes #27818. Item 1 (restore doors) is measured, pinned and fixed at this head. Item 2 asked for a decision on whether rows stored before the rule need an audit; comment 6512713420 on the card answers it at rule level: what os secret orphans covers, four readings it misses, what an operator can do today, and a report-only tooling change proposed outside the card's surface; no migration or repair is proposed. That is the decision the item asked for, so Fixes is acceptable. The card's trailing point for the maintainer to decide (whether a definition read should keep serving the reference to every reader) is not answered by this PR or the comment; it is a maintainer decision, not a seat's. The lane seat should carry it (a follow-up card, or the maintainer's word on the card) before or at close, so the close does not drop it.
  • Dogfood battery at fd02314: 4 reds, all in files this diff does not touch (error-sentences-live ×2, object-hook-lifecycle-scratch ×1 — the hook-abort answer, main red (Dogfood Regression Gate 1/3) since 21c4bdd8 (#27667): two dogfood pins still expect a code-less hook refusal after HOOK_ABORTED landed #27806; platform-app-object-entry-views ×1 — main-side). The PR body shows the same four red with this change's runtime code absent. Consistent with the tree; none from this diff.
  • Check-runs at this head: no red; the Test Core shards, Build Core, Type Check · workspace/consumer gates/debt ledger and several Lint · scoped jobs were still in progress when read. Landing waits for all green.
  • Author is objectstack-fleet[bot]; no outside-author note owed.

④ Design judgment

  • protocol-datasource-credentials-ref.ts: the right shape — one judge, one address read, one comparison; the new parameter changes words only, so the three doors cannot drift from the save door's rule. The restore and publish words say what each door may bring back and name the remedy. Holds against the cited code and objectql: a secret field accepts an existing secret: ref from a non-system write, so it can name another record's credential row #27582's shape.
  • protocol-authoring-gate.ts: the call sits in the save-door refusal family the restore doors already ask, with the one host member it needs (getOverlayRepo) added to the Pick; it is asked as an active write against the binding the restore writes under. Holds.
  • protocol.ts: one host line, one docblock bullet and one call-site block beside the other save-door judges the promotion re-asks, on the same promoteInto key; nothing moves (serial queue [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467). Holds.
  • Against the published spec on main: DatasourceSchema keeps external.credentialsRef authorable and documents that the door that binds a secret writes it. This narrowing makes that invariant real at the three doors that could otherwise revive a stored value the row no longer holds; no declared capability is taken from a consumer (the Console saves through the admin door; no SDK method sets the key).
  • Tests: the unit pin drives the real rollbackMetaItem, revertCommit and publishMetaItem over a stub held to the engine-double predicates, asserts code and status, a byte-identical store and the row's reference kept on every refusal, equal answers across the four refusal cases per door, no reference in any message, revertCommit's failed[] entry, and both acceptances per door; the dogfood pin holds the rule at the booted REST doors as a non-system caller. The ablation readings in the PR body match the pin's shape.

Implemented-by: claude/project-thread-rkqt9u
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #27860 · head fd02314b73a44b821cc9a3ccbf27f43114e3ac56 · 2026-10-30T02:39Z

维护者速读

The #27582 own-reference rule that #27817 set at the metadata save door now also holds at the doors that turn a stored body into the active datasource row: rollback, a commit revert's restore, and a draft publish (including a package publish). Each one refuses a body whose external.credentialsRef is not the reference its row holds now. The refusal is 400 VALIDATION_ERROR, gives one answer whatever the body names, and comes before anything is written. There are two visible side effects, both stated with their remedy in the changeset: a rollback to a version from before a credential rotation is refused, and so is a draft publish after one. Item 2 (rows stored before the fix) is answered as a read-only audit on the card (6512713420). Item 3 is the maintainer's and moves to #27884.

席位核对

ACCEPT

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

secrets: hold a datasource's restore doors and already-stored rows to the #27582 own-reference rule for external.credentialsRef (the residue of #27762)

1 participant