Repository navigation
fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one - #21942
Conversation
…ned datasource, and removes only a stored row under one isArtifactBacked now sees a datasource an installed code package declares, through a second non-standalone-artifact resolver read from the package records' declared datasources. The existing package door and the repository's write intent then refuse a PUT with NOT_OVERRIDABLE / 403 and the datasource row of the packaged-base regime table: the admin door's verdict (code-defined, cannot be edited at runtime, read-only) and its remedy (edit the *.datasource.ts source). A DELETE that would remove nothing is refused with the same verdict; a DELETE of a stored row under a code-defined name stays possible as repair, in the protocol's delete door and the repository's delete gate. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
… datasource, both kernel shapes Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…pair, and the refusal sweeps leave that tier to its own pins servedLockState's deletable reads whether the read found a stored row for an origin-gated code-defined item, so the read agrees with the door in both states: refused with no row, admitted (repair) with one. The two delete refusal sweeps derived from the registry flags exclude the origin-gated type, and the read-versus-door table measures that type's host-config removal at the protocol's delete door, where it is answered. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…and a pre-fix stored row is removable across restarts Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ed datasource refusal (narrowing) Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ta-door-code-datasource
…r test's pinned doubles Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 5ce9eece1832504aecb46ded9ebed94d3ada5621 && git checkout 5ce9eece1832504aecb46ded9ebed94d3ada5621
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 dd81fb50d5ad7bf85b6d9c65db325866a7890def && git checkout -B drift-repro 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 && git merge --no-ff dd81fb50d5ad7bf85b6d9c65db325866a7890def
node scripts/docs-audit/affected-docs.mjs --json 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8
|
ACCEPT (seat review) — PR #21942 at head
|
Fixes #21899
Clause-②: no (narrowing)
What changes
The metadata door now answers a code-defined datasource the way the published contract (
DatasourceSchema.origin: "code — authored as*.datasource.ts, GitOps-owned, read-only in the UI") and the datasource-admin door already did: read-only. Triage ruled Q1-A and Q2-B in 6006929054; this PR implements both, inpackages/metadata-protocolonly.isArtifactBacked(packages/metadata-protocol/src/protocol.ts) gains a second non-standalone-artifact resolver,isDeclaredCodeDatasource, in theisNestedArtifactFieldshape from org-override-registry-gate: thefieldoverlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743. It reads the installed packages' declareddatasources(registry.getAllPackages(), each record'smanifest.datasources, in the canonical array formdefineStackleaves).datasourceis added to that docblock's census. It never reads a MetadataService slot'soriginor a request body'sorigin; a unit case pins a body assertingorigin: 'runtime'on a code datasource as still refused, and one assertingorigin: 'code'on a runtime name as still saved.refusePackagedBaseOverrideon an environment kernel, and the repository write intent (override-artifactintoSysMetadataRepository.assertAllowed) on a host-config kernel, which is the showcase's shape. The answer isNOT_OVERRIDABLE/ 403. The sentence comes from the packaged-base sentence table (packaged-base-regime.ts), which gains oneorigin-gatedrow fordatasource. ADR-0126 §3 recordsdatasourceoutside the three regimes, as "origin-gated: code-defined read-only, runtime-created free", so the row is not a Regime C row. It carries no routes, only the owning source.DELETEthat would remove nothing is refused with the same verdict. ADELETEof an existing stored row answers 200. Where the carve-out lives:ObjectStackProtocolImplementation.originGatedRemovalRefusal(new, besiderefusePackagedBaseRemoval) holds the package door's removal verdict.deleteMetaItemanswers it at its row probe: it throws when no stored row exists and lifts it when one does.SysMetadataRepository.assertDeleteAllowedmirrors the lift for the same type throughisOriginGatedType. This is the topology-independent gate a host-config kernel asks.saveMetaItem's metadata: allowRuntimeCreate:false is not enforced — PUT /meta creates job and agent items the registry declares code-only #5086 record ("removing a code-only row that predates this refusal is repair, and must stay possible") and the A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960 ruling (removal restores the code-declared state, which is the narrowing direction). A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960's ownsupportsOverlayboundary is not widened:object, which sharesdatasource's registry flags, keeps refusing both verbs, and a guard pins that.servedLockStatereports what the doors do:editable: false, anddeletabletrue only while the read found a stored row to remove.The two doors' codes differ, by ruling
Triage's answer 6006929054: "The two doors' codes differ, and that is accepted. Each door speaks its own vocabulary; the verdict and the remedy agree." Measured on a real showcase boot at this branch:
PUT:403 NOT_OVERRIDABLE—Datasource 'showcase_external' is code-defined and cannot be edited at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy. See docs/adr/0062-external-datasource-runtime.md.PATCH:400 DATASOURCE_ADMIN_ERROR—Datasource 'showcase_external' is code-defined and cannot be edited at runtime.DELETEwith no stored row:403 NOT_OVERRIDABLE—Datasource 'showcase_external' is code-defined and cannot be removed at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy. See docs/adr/0062-external-datasource-runtime.md.DELETE:400 DATASOURCE_ADMIN_ERROR—Datasource 'showcase_external' is code-defined and cannot be removed at runtime.The host's
defaultdatasource (H4): the admin door treats it as code-defined; covering it here is a named gapbootStack, admin routes mounted asserve.tsmounts them):PATCH /api/v1/datasources/defaultanswers400 DATASOURCE_ADMIN_ERROR"Datasource 'default' is code-defined and cannot be edited at runtime.", andDELETEanswers "… cannot be removed at runtime.".PUT /api/v1/meta/datasource/defaultanswers 200 "Saved datasource 'default' (env-wide, state=active)" and the read then serves the edit.DELETEanswers 200.defaultunchanged:PUTanswers 200 on this branch too, which is measured.metadata-protocolwithout aruntimeorservice-datasourcechange:DefaultDatasourcePluginregistersdefaultonly throughMetadataService.registerInMemory. That is the slot whoseorigintriage ruled unsound, because a stored row overwrites it.ConnectResult: name, status, reason, ownership).listDatasourceDefs()mixes code and runtime definitions.default.runtimeorservice-datasourcefile is edited. This is reported for a follow-up card, and the changeset names it.Pins, before and after (real showcase boot,
showcase_external)"Before" is the reverse-verification leg below (the base
isArtifactBackedon committed HEAD) and the first run's measurements at54fb60ac3f(6006105473). "After" is this branch.PUT /meta/datasource/showcase_externalNOT_OVERRIDABLEwith the verdict and remedy above; nosys_metadatarow; the read serves the code labelDELETE, no stored rowNOT_OVERRIDABLE, "cannot be removed at runtime"DELETE, a pre-existing stored row (seeded as a pre-fix save wrote it, across a restart)DELETEanswers 403code,_packageIdcom.example.showcase; no rowPOST201,PATCH200,DELETE204; metadata door:PUT200,PUT200,DELETE200Reverse verification
Run on committed HEAD
8413b4622d, throughscripts/ablation-replace.mjs(WRAP mode, its own restore trap, plus agit checkout HEADtrap):isArtifactBacked's last line, dropping|| this.isDeclaredCodeDatasource(type, name). On disk the anchor went 1 to 0 and the replacement 0 to 1. The blob went8e2d759618bato51f36f712468.protocol.code-defined-datasource-door.test.tsshowed 14 failed and 11 passed. The red cases are the resolver,PUTrefused (both kernels),DELETEwith no row refused, the repair's secondDELETE, the read envelope, and the 500-character bound. The green cases are the runtime controls, the hatch guard and the repository gate cases, which do not route throughisArtifactBacked.pnpm --filter @objectstack/metadata-protocol buildemitted ESM/CJS and failed only DTS on TS6133, because the mutation leaves the new method unused.node scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol '...' --absentconfirmed the marker absent from all 22 built files.meta-door-code-datasource.dogfood.test.tsshowed 4 failed and 2 passed. ThePUTpin readexpected { status: 200, code: undefined } to deeply equal { status: 403, code: 'NOT_OVERRIDABLE' }.8e2d759618ba),git diff HEADis empty, andgit status --porcelainis empty. A rebuild put the marker back in both built entry files (preflight: present). The unit file then passed 25/25 and the dogfood file 6/6.Tests (HEAD
dd81fb50d5)pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 217 files passed (3 skipped), 27941 tests passed.pnpm --filter @objectstack/metadata-protocol typecheckandpnpm --filter @objectstack/dogfood typecheckare green.tsc --listFilesincludes every touched test file.meta-door-code-datasource.dogfood.test.ts(6/6) andexternal-import-code-datasource-namespace.dogfood.test.ts./metadoor were run, all green:runtime:datasource-visibility,meta-type-write-capability-parity,stored-metadata-reader-contexts.pin,standalone-stack-hydrate-metadata,meta-write-org-scope,dispatcher-plugin.declared-5xx-prose-withhold.rest:meta-type-read-capability,meta-type-write-capability,rest-server-meta-write-org-scope,meta-unknown-type-read-refusal,rest-server-meta-org-scope-url-spelling,rest.service-datasource:datasource-admin-record-judgement.objectql:overlay-precedence.datasourcedelete refusal and were triaged.protocol.delete-rewrap-envelope,protocol.legacy-overlay-deleteandprotocol.read-lock-flags-write-doorexclude the origin-gated type from the derived refusal sweeps or measure it at the protocol's delete door. Each change points at the new pin file.Gates (HEAD
dd81fb50d5)node scripts/pm/dispatch-gates.mjs --commandsderived 76 commands, and all 76 were run with exit 0.--ranreconciliation reports "76 derived famil(ies) accounted for — 76 run, 0 NOT-MEASURED".check:engine-double-contractasked for its ledger to learn the new file's pinned doubles (--write, +3 rows, committed).check-closing-target-claim,check-partof-closing-keywordandcheck-single-claim-pathsexited 2 with no PR context (NOT MEASURED locally); they run on this PR in CI.check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchors,check:adr-anchors) exited 0.check:adr-0087-registration --base origin/main: one declared-breaking changeset,not-required (no-migration-prescription).check-changeset-no-majorreports no major.Acceptance notes
DELETE. The read keeps serving the stored copy until the next restart, because the datasource-admin plugin's boot restore registered it in the MetadataService. The receipt still reads "reset to artifact default". That is finding(service-datasource): a stored datasource row overrides a code-defined datasource at boot, so after a restart the admin door serves and edits it at runtime (restoreRuntimeDatasources has no code-collision check) #21922's in-memory half, so the repair pin holds across a restart. Measured: in the same boot,GETafter the repair served "Shadow 21899"; after the restart it served the code label.409 METADATA_CONFLICT, whether or not the version token is sent, because the admin door'ssys_metadatarow carries a null checksum. This is pre-existing and untouched here (runtime names are not artifact-backed). It is reported for filing in the dev report. The PM's hypothesis that sending the version makes it pass was measured false.origin-gatedrow rather than a Regime C row, per ADR-0126 §3. Its module header now scopes the "no redeploy prescription" rule to Regime C sentences.OS_METADATA_WRITABLE=datasourcestill opens the lock exactly as before; a guard case pins it.datasourcesno package body declares (AppPluginwarns about this composition at boot) registers code datasources the resolver does not see, because no package record carries them.Changeset
.changeset/21899-meta-door-code-datasource-read-only.md:@objectstack/metadata-protocolminor, BREAKING,Clause-②: no (narrowing). It states the remedy: edit the*.datasource.tssource, and delete a stored row through the metadata door to repair. It carries one ADR-0087 marker.Generated by Claude Code