Repository navigation
Commit 9cc2c79
fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one (#21942)
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, in `packages/metadata-protocol`
only.
- **The resolver (Q1-A).** `isArtifactBacked`
(`packages/metadata-protocol/src/protocol.ts`) gains a second
non-standalone-artifact resolver, `isDeclaredCodeDatasource`, in the
`isNestedArtifactField` shape from #7743. It reads the installed
packages' declared `datasources` (`registry.getAllPackages()`, each
record's `manifest.datasources`, in the canonical array form
`defineStack` leaves). `datasource` is added to that docblock's census.
It never reads a MetadataService slot's `origin` or a request body's
`origin`; a unit case pins a body asserting `origin: 'runtime'` on a
code datasource as still refused, and one asserting `origin: 'code'` on
a runtime name as still saved.
- **The package door's answer.** With the resolver in place, the
existing door refuses the save on both kernel shapes:
`refusePackagedBaseOverride` on an environment kernel, and the
repository write intent (`override-artifact` into
`SysMetadataRepository.assertAllowed`) on a host-config kernel, which is
the showcase's shape. The answer is `NOT_OVERRIDABLE` / 403. The
sentence comes from the packaged-base sentence table
(`packaged-base-regime.ts`), which gains one `origin-gated` row for
`datasource`. ADR-0126 §3 records `datasource` outside 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.
- **DELETE as repair (Q2-B).** A `DELETE` that would remove nothing is
refused with the same verdict. A `DELETE` of an existing stored row
answers 200. Where the carve-out lives:
- `ObjectStackProtocolImplementation.originGatedRemovalRefusal` (new,
beside `refusePackagedBaseRemoval`) holds the package door's removal
verdict. `deleteMetaItem` answers it at its row probe: it throws when no
stored row exists and lifts it when one does.
- `SysMetadataRepository.assertDeleteAllowed` mirrors the lift for the
same type through `isOriginGatedType`. This is the topology-independent
gate a host-config kernel asks.
- The reasoning is reused, not invented. It is `saveMetaItem`'s #5086
record ("removing a code-only row that predates this refusal is repair,
and must stay possible") and the #6960 ruling (removal restores the
code-declared state, which is the narrowing direction). #6960's own
`supportsOverlay` boundary is not widened: `object`, which shares
`datasource`'s registry flags, keeps refusing both verbs, and a guard
pins that.
- **The read envelope.** `servedLockState` reports what the doors do:
`editable: false`, and `deletable` true 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:
- metadata door `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.`
- admin door `PATCH`: `400 DATASOURCE_ADMIN_ERROR` — `Datasource
'showcase_external' is code-defined and cannot be edited at runtime.`
- metadata door `DELETE` with 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.`
- admin door `DELETE`: `400 DATASOURCE_ADMIN_ERROR` — `Datasource
'showcase_external' is code-defined and cannot be removed at runtime.`
## The host's `default` datasource (H4): the admin door treats it as
code-defined; covering it here is a named gap
- **Measured** on a real showcase boot (`bootStack`, admin routes
mounted as `serve.ts` mounts them):
- `PATCH /api/v1/datasources/default` answers `400
DATASOURCE_ADMIN_ERROR` "Datasource 'default' is code-defined and cannot
be edited at runtime.", and `DELETE` answers "… cannot be removed at
runtime.".
- On the metadata door, `PUT /api/v1/meta/datasource/default` answers
200 "Saved datasource 'default' (env-wide, state=active)" and the read
then serves the edit. `DELETE` answers 200.
- This PR leaves `default` unchanged: `PUT` answers 200 on this branch
too, which is measured.
- **Why it is not covered here.** The host's code datasource set is not
readable from `metadata-protocol` without a `runtime` or
`service-datasource` change:
- `DefaultDatasourcePlugin` registers `default` only through
`MetadataService.registerInMemory`. That is the slot whose `origin`
triage ruled unsound, because a stored row overwrites it.
- The connection service's retained state carries no origin
(`ConnectResult`: name, status, reason, ownership).
- The engine's `listDatasourceDefs()` mixes code and runtime
definitions.
- No package declares `default`.
- **What follows.** Under the claim's stop condition, no `runtime` or
`service-datasource` file 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
`isArtifactBacked` on committed HEAD) and the first run's measurements
at `54fb60ac3f` (6006105473). "After" is this branch.
| Pin | Before | After |
|:--|:--|:--|
| `PUT /meta/datasource/showcase_external` | 200 "Saved datasource
'showcase_external' (env-wide, state=active)", a row persisted, the read
served the edit | 403 `NOT_OVERRIDABLE` with the verdict and remedy
above; no `sys_metadata` row; the read serves the code label |
| `DELETE`, no stored row | 200 "No datasource 'showcase_external' found
- nothing to delete." | 403 `NOT_OVERRIDABLE`, "cannot be removed at
runtime" |
| `DELETE`, a pre-existing stored row (seeded as a pre-fix save wrote
it, across a restart) | 200, reset true | 200, reset true, row gone; a
second `DELETE` answers 403 |
| after the repair and one more restart | — | both doors serve "External
Analytics (SQLite)", origin `code`, `_packageId` `com.example.showcase`;
no row |
| runtime datasource | admin door: `POST` 201, `PATCH` 200, `DELETE`
204; metadata door: `PUT` 200, `PUT` 200, `DELETE` 200 | the same |
## Reverse verification
Run on committed HEAD `8413b4622d`, through
`scripts/ablation-replace.mjs` (WRAP mode, its own restore trap, plus a
`git checkout HEAD` trap):
- **The mutation** restores the base form of `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 went
`8e2d759618ba` to `51f36f712468`.
- **Unit suite (src):** `protocol.code-defined-datasource-door.test.ts`
showed 14 failed and 11 passed. The red cases are the resolver, `PUT`
refused (both kernels), `DELETE` with no row refused, the repair's
second `DELETE`, 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 through `isArtifactBacked`.
- **Dogfood (dist):**
- `pnpm --filter @objectstack/metadata-protocol build` emitted 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 '...' --absent` confirmed the marker
absent from all 22 built files.
- `meta-door-code-datasource.dogfood.test.ts` showed 4 failed and 2
passed. The `PUT` pin read `expected { status: 200, code: undefined } to
deeply equal { status: 403, code: 'NOT_OVERRIDABLE' }`.
- **Restore:** the blob after restore equals HEAD (`8e2d759618ba`), `git
diff HEAD` is empty, and `git status --porcelain` is 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 typecheck` and `pnpm
--filter @objectstack/dogfood typecheck` are green. `tsc --listFiles`
includes every touched test file.
- Dogfood: `meta-door-code-datasource.dogfood.test.ts` (6/6) and
`external-import-code-datasource-namespace.dogfood.test.ts`.
- Consumer files touching the datasource `/meta` door 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`.
- Three existing sweeps had pinned the old `datasource` delete refusal
and were triaged. `protocol.delete-rewrap-envelope`,
`protocol.legacy-overlay-delete` and
`protocol.read-lock-flags-write-door` exclude 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 --commands` derived 76 commands,
and all 76 were run with exit 0. `--ran` reconciliation reports "76
derived famil(ies) accounted for — 76 run, 0 NOT-MEASURED".
- `check:engine-double-contract` asked for its ledger to learn the new
file's pinned doubles (`--write`, +3 rows, committed).
- The artifact-roster block (53) ran: 50 exited 0.
`check-closing-target-claim`, `check-partof-closing-keyword` and
`check-single-claim-paths` exited 2 with no PR context (NOT MEASURED
locally); they run on this PR in CI.
- The four symbol-anchor sweeps (`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-major` reports no major.
## Acceptance notes
- **Same boot after the repair `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 #21922's in-memory
half, so the repair pin holds across a restart. Measured: in the same
boot, `GET` after the repair served "Shadow 21899"; after the restart it
served the code label.
- **An admin-created runtime datasource cannot be edited or deleted
through the metadata door.** Both answer `409 METADATA_CONFLICT`,
whether or not the version token is sent, because the admin door's
`sys_metadata` row 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.
- **H2's "packaged-base sentence table".** The table gains an
`origin-gated` row rather than a Regime C row, per ADR-0126 §3. Its
module header now scopes the "no redeploy prescription" rule to Regime C
sentences.
- **The operator hatch.** `OS_METADATA_WRITABLE=datasource` still opens
the lock exactly as before; a guard case pins it.
- **Not measured.** An artifact whose top-level `datasources` no package
body declares (`AppPlugin` warns 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-protocol` minor, BREAKING, `Clause-②: no
(narrowing)`. It states the remedy: edit the `*.datasource.ts` source,
and delete a stored row through the metadata door to repair. It carries
one ADR-0087 marker.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1f04696 commit 9cc2c79
10 files changed
Lines changed: 1080 additions & 55 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- qa/dogfood/test
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
66 | 87 | | |
67 | 88 | | |
68 | | - | |
69 | | - | |
70 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
71 | 94 | | |
72 | 95 | | |
73 | 96 | | |
74 | 97 | | |
75 | | - | |
76 | | - | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
77 | 104 | | |
78 | 105 | | |
79 | 106 | | |
| |||
86 | 113 | | |
87 | 114 | | |
88 | 115 | | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
94 | 131 | | |
95 | 132 | | |
96 | 133 | | |
| |||
117 | 154 | | |
118 | 155 | | |
119 | 156 | | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
120 | 163 | | |
121 | 164 | | |
122 | 165 | | |
| |||
128 | 171 | | |
129 | 172 | | |
130 | 173 | | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
137 | 179 | | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
153 | 208 | | |
154 | 209 | | |
155 | | - | |
156 | | - | |
157 | | - | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
158 | 213 | | |
159 | 214 | | |
160 | 215 | | |
161 | | - | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
162 | 230 | | |
163 | 231 | | |
164 | 232 | | |
| |||
167 | 235 | | |
168 | 236 | | |
169 | 237 | | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
170 | 244 | | |
171 | 245 | | |
172 | 246 | | |
173 | | - | |
174 | | - | |
175 | | - | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
176 | 251 | | |
177 | 252 | | |
178 | 253 | | |
179 | 254 | | |
180 | 255 | | |
181 | | - | |
182 | | - | |
183 | | - | |
184 | | - | |
185 | | - | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
186 | 264 | | |
0 commit comments