Repository navigation
fix(metadata-protocol)!: a datasource restored or published from a stored body may name only the credentials reference its row holds now - #27860
Conversation
…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>
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Contract-review-claim: The lane seat runs the |
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Seat ACCEPT · PR #27860 · head
|
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
datasourcebody whoseexternal.credentialsRefis 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/**andcontent/docs/**teachexternal.credentialsRefonly 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 拒绝,什么都不写,无论引用指向什么答复都相同,消息里不出现引用值。secret:ref from a non-system write, so it can name another record's credential row #27582 的规则。第 2 项(修复前已存的数据)的只读审计已作为卡上评论发出。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 通过。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.
datasourceCredentialsRefSaveRefusalgains an optionaldoor: '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 nosource, so the comparison is with that row's current reference.rollbackMetaItemandrevertCommit's restore limb both reach the restore path's save-door refusal family,refuseRestoredBodyTheSaveDoorRefusesinprotocol-authoring-gate.ts. The judge is asked there, afterunknownDatasourceSaveRefusal, in the save door's order, againstbinding, the package binding of the active row the version is restored into. That family runs insiderestoreVersionbefore the active row is read and before the put, so a refused version writes nothing.protocol.tsgains one host line (getOverlayRepo) and one docblock bullet.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 afterunknownDatasourceSaveRefusal, againstpromoteInto, the binding the active row is written under, beforerepo.promoteDraft. One block inprotocol.ts; nothing in it moves.sourcepass-through are unchanged.Why
The card's item 1, measured on
origin/mainfec60e0 with #27817's head (4258efa) merged in: each of the three doors wrote a stored datasource body as the active row with whateverexternal.credentialsRefit 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 orphanscovers, 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 realrollbackMetaItem,revertCommitandpublishMetaItemover a stub engine that stores rows and history, the double ofprotocol.restore-doors-save-refusal-family.test.ts.codeandstatusasserted, 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.revertCommitreports it infailed[]withcode: 'VALIDATION_ERROR'.packages/qa/dogfood/test/datasource-credentials-ref-restore-doors.dogfood.test.ts(new, 4 cases): the same rule atPOST /api/v1/meta/datasource/:name/rollbackand/publishof 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
datasourcerow without passingsaveMetaItem'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.rollbackToPackageCommitrunsrevertCommitper commit and counts a commit as reverted when it does not throw. A refused item lands in that inner revert'sfailed[], 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.tsgains one host line, one docblock bullet and one call-site block; nothing in it moves or reflows (serial queue [maintainer] refactor(metadata-protocol): splitsrc/protocol.ts(33,065 lines, +10,169 in 30 days) along itsObjectStackProtocolImplementationseams, one move-only slice per PR #25467). The restore call site is inprotocol-authoring-gate.ts, the restore helper beside it.scripts/engine-double-contract.pinned.jsonis outside the declared file surface: the gate requires the new pin's engine double in the ledger.Metadata-protocol: narrowed(three doors refuse a body they wrote), hence aminorbump on@objectstack/metadata-protocoland 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 typecheckexit 0;vitest runover 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/docsat fd02314: 114/114 tasks, exit 0.node scripts/pm/dispatch-gates.mjs --ranat fd02314: 73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.check-plugin-teardown-shape --self-testfirst exited 3 (its fixture commit is outside the shallow clone); after fetching that commit it exited 0.pnpm lintexit 0.scripts/ablation-replace.mjs(mutate, run the pin, restore; the restore proven blob-equal to HEAD withgit diff HEADempty):source, taking the pass-through → 10 failed | 11 passed;@objectstack/metadata-protocolrebuilt,scripts/ablation-dist-preflight.mjsproved both markers indist/(exit 0), the dogfood pin 2 failed | 2 passed (4) (the two refusals); both files restored (blob equal to HEAD), rebuilt, preflight--absentexit 0 for both markers with the tree clean, the pin 4 passed (4).doorwas 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).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 onmain's code too:error-sentences-live.dogfood.test.ts(2) andobject-hook-lifecycle-scratch.dogfood.test.ts(1): the hook-abort answer, tracked as 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.test.ts(1): a Setup entry namessys_exchange_rate, which neither source the test reads declares (the object landed in 194e66c).packages/metadata-protocol/srcwas put back to the merge base 7b8bcfe (this change's runtime code absent from the rebuiltdist/) and the three files read 4 failed | 56 passed (60), the same four tests. The tree was then restored (git diff HEADempty, status clean) and rebuilt.Human Minimum
packages/specholds no runtime logic. (not touched)pnpm lintpasses.main, one pull request.packages/spectouched: no.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4d1qRCNJhpDgVXFoXkp