Repository navigation
fix(metadata-protocol)!: a datasource saved through the metadata door may name only its own stored credentials reference - #27817
Conversation
…s credentialsRef on the metadata save door A non-system save of a datasource may name, in external.credentialsRef, only the reference its own stored row already holds. Committed before the fix: on this tree the ten refusal cases are red and the four accepted cases green. Claude-Session: https://claude.ai/code/session_016WV4cx2wNeyC4kD8R4P7Ns Co-authored-by: Claude <noreply@anthropic.com>
…ed credentials reference A non-system save of a datasource through the metadata save door may name, in external.credentialsRef, only the reference its own stored row already holds (the own-reference rule). Anything else is refused with VALIDATION_ERROR / 400 before anything is written, with one answer whatever the reference names. Server-stated rewrites keep their pass-through. The judge is a helper beside protocol.ts; protocol.ts gains its import and one call site. Claude-Session: https://claude.ai/code/session_016WV4cx2wNeyC4kD8R4P7Ns Co-authored-by: Claude <noreply@anthropic.com>
…t the REST metadata door; changeset Claude-Session: https://claude.ai/code/session_016WV4cx2wNeyC4kD8R4P7Ns Co-authored-by: Claude <noreply@anthropic.com>
…ss read only The save door binds its repository under the one spelling the stored-row write census reads, and the judge receives only `headAt`, so it can read the save's own row and write nothing. Claude-Session: https://claude.ai/code/session_016WV4cx2wNeyC4kD8R4P7Ns Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 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 2a542de9c6e71766afaa9a9f74b2314660794972 && git checkout 2a542de9c6e71766afaa9a9f74b2314660794972
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b83baed2b35fbeebfdaeda07943da1e91d904aef 4258efac241ae1e557a984b3487e81b026ac7425 && git checkout -B drift-repro b83baed2b35fbeebfdaeda07943da1e91d904aef && git merge --no-ff 4258efac241ae1e557a984b3487e81b026ac7425
node scripts/docs-audit/affected-docs.mjs --json b83baed2b35fbeebfdaeda07943da1e91d904aef
|
…he caller's bound; record its engine double in the ledger The stub engine's `find` applies `limit` after the filter, as the ObjectQL double-limit gate asks of a new double, and the engine-double ledger records the pin's `findOne` and `update` seams (regenerated with `check-engine-double-contract.mjs --write`). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cx2wNeyC4kD8R4P7Ns
7256944 to
4258efa
Compare
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Contract-review-claim: The lane seat runs the |
|
Skills-docs: none — Dogfood battery — The three red tests are not this PR's. They are red on
All three expect an undeclared hook abort on the Commit note. The PR body names the gate-fix commit as 7256944. It was reworded to 4258efa to correct its co-author trailer. The tree is identical. The CI failures reported on 7256944 are aggregator jobs whose lanes were cancelled by that push. Generated by Claude Code |
|
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 #27817 · head
|
Requested by Elon · project thread
Fixes #27762
domain:server#3 · os-elon-musk · session_016WV4cx2wNeyC4kD8R4P7Ns (card thread)
Clause-②: no (narrowing) — the metadata save door refuses a
datasourcebody whoseexternal.credentialsRefis not the reference its own stored row already holds, where it stored the value as given before. Nothing is widened.Metadata-protocol: narrowed
Skills-docs: none —
skills/**andcontent/docs/**teachexternal.credentialsRefonly on datasources declared in code (defineDatasource,*.datasource.ts), which the metadata save door does not serve (a code-defined name is refused there by the package door); none teaches naming a reference on a runtime metadata save, and the refusal itself names the door that binds one.维护者速读
PUT /api/v1/meta/datasource/:name、运行时/meta门、MCP 与进程内保存,同一个保存入口)保存数据源时,external.credentialsRef只能是这个数据源自己已存记录里的那个引用;新建时带引用、编辑时换成别的引用、或记录本没有引用却带了一个,一律 400 拒绝,且什么都不写,无论引用指向什么答复都相同,消息里不出现引用值。secret:ref from a non-system write, so it can name another record's credential row #27582 的规则审第三个凭据引用写入口:「非系统写入只能封存新值,或保留本记录自己已存的引用」。实测这个门会原样保存请求带来的引用,不符合规则。packages/metadata-protocol/src/protocol-datasource-credentials-ref.ts(规则本身),packages/metadata-protocol/src/protocol.ts中saveMetaItem的那一处调用,两个新测试文件。pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.datasource-credentials-ref-own-reference.test.ts,预期 14/14 通过。What changes
The rule, as #27582 set it for
secretfields (PR #27695) and #27722 applied it to the datasource admin door's create (PR #27761): a non-system write may only seal a new value or keep its own record's stored reference. The metadata save door seals nothing, so the one reference a save through it may name is the one its own stored row already holds.datasourceCredentialsRefSaveRefusal, a new module besideprotocol.ts(protocol-datasource-credentials-ref.ts). For adatasourcebody that namesexternal.credentialsRef, it reads the row the save writes, the way the lock-family judge reads it: the repository's write address for the save's state, package and bundle member, and for a draft with no draft row yet, the active row it overlays. The save is accepted only when that row holds exactly the same reference. It is handed the repository'sheadAtread alone, so it can read and cannot write.VALIDATION_ERROR/ 400, the standard-catalog pair this door's own name refusal and the objectql: a secret field accepts an existingsecret:ref from a non-system write, so it can name another record's credential row #27582 refusal already use. It is thrown before anything is written. The message names the key and the datasource, never the value, and nothing about the reference is looked up, so the answer is the same whatever the reference names. It names the remedy: send back the reference a read serves, omit the key, or bind a secret through the datasource admin door.saveMetaItem, beside the other datasource-family save refusals: after the schema gate (the body is a valid datasource, socredentialsRefis a string) and before the authoring gates and every write.protocol.tsgains its import and one call site; nothing in it moves.sourceset: the stored-metadata migration) keep the pass-through, as the system principal does under the objectql: a secret field accepts an existingsecret:ref from a non-system write, so it can name another record's credential row #27582 rule. Every other save is judged: the RESTPUTdoors (meta-envelope), the runtime dispatcher's/metadoor (meta-dispatch), MCP and in-process authoring, and the package copy (package-duplicate) and unmanaged import (package-unmanaged-import), which write new rows that hold no reference of their own.Why
The card's measurement, on
origin/mainat 968cacf: a non-system save through this door stored a datasource body'sexternal.credentialsRefas given — on a create, on an edit naming another datasource's reference, and on a draft. The pin below was committed first (d816620) and was red on that tree in all ten refusal cases.Tests
packages/metadata-protocol/src/protocol.datasource-credentials-ref-own-reference.test.ts(new, 14 cases): the realsaveMetaItemover a stub engine.codeandstatusasserted, the store byte-identical afterwards and no write issued: a create naming a reference another datasource holds, and one naming a reference no row holds (identical answers); an edit naming a reference other than its own (identical answers for a held and an unheld one); an edit naming one where its row holds none; a draft naming one the active row does not hold; the plural type spelling; and each face —meta-envelope,meta-dispatch,package-duplicate,package-unmanaged-import, an MCP save. No refusal message contains any reference.source: 'migrate-stored'pass-through.packages/qa/dogfood/test/datasource-credentials-ref-own-reference.dogfood.test.ts(new, 3 cases): the same rule atPUT /api/v1/meta/datasource/:nameof a booted stack, as a signed-in non-system caller — a create refused with one answer and nothing stored, an edit refused with the stored reference kept, and the round trip accepted with the reference kept.The pin's stub engine is held to the engine-double gates: its
findapplies the caller'slimitafter the filter, andscripts/engine-double-contract.pinned.jsonrecords itsfindOneandupdateseams (regenerated with--write; 2 rows added, none lost).Readings: the unit pin on
origin/main968cacf, before the fix, 10 failed | 4 passed (14) (the four that passed are the accepted cases). With the fix: 14 passed (14); the dogfood pin 3 passed (3).Acceptance notes
failed[]with the refusal text and the rest of the package copies on; in an import the item is refused the same way.protocol.tsgains one import 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).Metadata-protocol: narrowed(the save door refuses a body it accepted), 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 7e9b721 (main merged in): 281 passed | 4 skipped (285) files, 21816 passed | 26 skipped tests, exit 0.pnpm exec turbo run build --concurrency=2 --filter=!@objectstack/docsat 7e9b721: 114/114 tasks, exit 0.node scripts/pm/dispatch-gates.mjs --ran: 73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. 63 families ran at 7e9b721;check:engine-double-contractandcheck:objectql-double-limitwere red there on the new pin's stub engine, were fixed in 7256944 and re-ran green with the 8 families that commit'sscripts/file adds.pnpm lintexit 0.scripts/ablation-replace.mjs(mutate, run the pin, restore; the restore proven blob-equal to HEAD):ablation-replacebefore any test ran — its replacement was a substring of its anchor, so the count could not rise — and applied no mutation; it was redone with a distinct anchor.)protocol.ts,@objectstack/metadata-protocolrebuilt,scripts/ablation-dist-preflight.mjsproved the marker indist/(exit 0), the dogfood pin 3 failed (3); restored (git diffempty), rebuilt, preflight--absentexit 0, the pin 3 passed (3);protocol.tsblob equal to HEAD.pnpm --filter @objectstack/dogfood test, the whole battery, at 7e9b721): 354 passed | 2 failed | 3 skipped (359) files, 2867 passed | 3 failed | 20 skipped (2890) tests. The three failures are not this change's:error-sentences-live.dogfood.test.ts(2) andobject-hook-lifecycle-scratch.dogfood.test.ts(1) assert that an undeclared hook abort on the/datadoor answers with nocode, and fix(rest): an undeclared hook abort answers the data door with HOOK_ABORTED #27667 (21c4bdd, onmain) now answersHOOK_ABORTEDthere; neither test is changed onorigin/mainat c5f4a06. The commit after 7e9b721 touches only a unit-test double and a gate ledger, nothing the battery loads.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_016WV4cx2wNeyC4kD8R4P7Ns