Skip to content

Commit ebf7d98

Browse files
huangyiireneclaude
andauthored
fix(metadata-protocol): the metadata write refusal reports the package door — ITEM_LOCKED / WRITABLE_PACKAGE_REQUIRED are emitted where they apply (#8185)
* fix(metadata-protocol): the write refusal reports the package door, not only the type's flags (#7682) `PUT /api/v1/meta/object/showcase_task` answered `403 NOT_OVERRIDABLE` identically whether `?package=` named a read-only or a writable package: `SysMetadataRepository.assertAllowed` discriminated on the metadata TYPE's registry flags and never read the base the caller named. `ITEM_LOCKED` and `WRITABLE_PACKAGE_REQUIRED` are both registered to this package in the error-code ledger and neither was ever emitted on this path. The refusal branch now reads the named base through the shared `isWritablePackage` predicate (imported from `package-writability.ts`, the same one `saveMetaItem`'s ADR-0070 D1 gate and the `/packages` lifecycle gate use): * `override-artifact` into a read-only base -> 403 ITEM_LOCKED, carrying ADR-0010's reserved `lockSource: 'package'` and the package id. "Pick a writable base" would be a false prescription here, so the code names the lock instead. * `runtime-only` into a read-only base -> 422 WRITABLE_PACKAGE_REQUIRED, the same code, status and prescription `saveMetaItem` already emits for exactly this condition, now also stated at the single persistence route. No ALLOW decision moves: every allow limb returns before this point, so an ADR-0005 overlay (which names the read-only package it customizes by construction) and the documented `OS_METADATA_WRITABLE` hatch behave exactly as before -- both pinned, the hatch because #8146 must be free to move it deliberately. A write naming no base keeps the previous codes verbatim, and the DELETE verb is untouched (#6960). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmES43BMDg4bPrxTdi5q7t * docs(metadata-protocol): mark the OS_METADATA_WRITABLE case as CONTESTED, per the 2026-08-12 #8146 ruling An in-flight maintainer ruling landed on #7682 after this work started: #8146 is settled as option B — a hatch write into a read-only package should REFUSE, the Studio badge is telling the truth, and the hatch is type-level by its own shipped documentation. The dispatch's instruction to "preserve and pin" that behaviour is therefore superseded, and the ruling says explicitly not to land a pin asserting it as correct. No behaviour change here. The hatch limb was never touched by this PR (it returns before the new package door), so nothing to revert; what changes is the CLAIM the suite and the changeset were making about it. The case is relabelled as a characterization pin of today's answer and kept, deliberately, as the tripwire the #8146 fix must invert. Re-measured on current main at that ruling's request, since the original measurement was against two-week-old builds: it still reproduces end to end through saveMetaItem on the host-config topology — `success: true`, and the row lands with `package_id = com.example.showcase`, i.e. bound INTO the read-only package rather than as the per-org override the variable's documentation describes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmES43BMDg4bPrxTdi5q7t * test(metadata-protocol): drop the OS_METADATA_WRITABLE case, record the gap as deliberate (#7682) PM ruling on PR #8185's patch round. The case asserted that a hatch write into a read-only package SUCCEEDS — green because the bug is present — and the 2026-08-12 ruling on #8146 said "don't merge it". A better label does not change what the assertion claims, so it goes. My tripwire argument for keeping it is answered: #8146 already names "the refusal plus a rejection pin asserting code and status" as its own deliverable, so the pin gets written against the FIXED behaviour, which is where it belongs. The opposite risk is real too — a deliberately-red pin gets "repaired" to green by someone who never read why it was there. The suite docblock now states that the hatch path is deliberately uncovered pending #8146, so the absence reads as a decision rather than an oversight, and carries the re-measurement: the row lands bound INTO the read-only package (package_id = com.example.showcase, organization_id = null) rather than as the per-org override the variable's own documentation describes. The changeset says the same. No source change: `assertAllowed` is correct as shipped, and both open questions were ruled in its favour (the ITEM_LOCKED / WRITABLE_PACKAGE_REQUIRED partition, and `Part of #7682`). The `permission` case is kept and moved beside its siblings — with no hatch set it asserts the refusal, which is the card's point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmES43BMDg4bPrxTdi5q7t * docs(metadata-protocol): keep the delete bullet with its list in the suite docblock (#7682) Formatting only, no assertion or source change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmES43BMDg4bPrxTdi5q7t * fix(metadata-protocol): type the test helper's `type` as MetaRef['type'] (#7682) CI's TypeScript Type Check went red on my push: the metadata-protocol DEBT entry records 63 raw tsc errors and `tsc --noEmit` reported 64. The +1 was mine and real: sys-metadata-repository.package-writability.test.ts(166,21): error TS2322: Type 'string' is not assignable to type '"object" | "view" | ... | "email_template"'. `putWith`'s `opts.type` was `string`; `MetaRef['type']` is a literal union. The suite ran green under vitest either way — this package has no `typecheck` script, so its type surface is only judged by the ledger in CI, which is exactly the gap that let a widened parameter through a local run. Fixed at the source, not in the ledger: DEBT is a ratchet that may only shrink, and raising an entry for a file added in the same PR would be the worst version of that. `tsc --noEmit` now reports 63 for the package, matching the recorded entry, with zero errors attributable to either file this PR touches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmES43BMDg4bPrxTdi5q7t --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 59ed2e9 commit ebf7d98

3 files changed

Lines changed: 595 additions & 2 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): the metadata write refusal reports the package door — `ITEM_LOCKED` / `WRITABLE_PACKAGE_REQUIRED` are emitted where they apply (#7682)
6+
7+
`PUT /api/v1/meta/object/showcase_task` answered `403 NOT_OVERRIDABLE`
8+
("'object' is not allowOrgOverride in the registry") — the **same** code, status
9+
and sentence whether `?package=` pointed at a **read-only** package or a
10+
**writable** one. The refusal discriminated on the metadata TYPE's registry
11+
flags and never read the base the caller named, so the two codes the error-code
12+
ledger registers to this package for the package-writability condition —
13+
`ITEM_LOCKED` and `WRITABLE_PACKAGE_REQUIRED` — were never emitted on this path
14+
at all. Declared, not enforced.
15+
16+
`SysMetadataRepository.assertAllowed` now reads the named base through the
17+
shared `isWritablePackage` predicate (the same one `saveMetaItem`'s ADR-0070 D1
18+
gate and the `/packages` lifecycle gate use — imported, not re-spelled), and a
19+
refused write that named a read-only base says so:
20+
21+
- **`override-artifact`** (an artifact backs the name, and it ships from a
22+
package the deployment provides) → `403 ITEM_LOCKED`, carrying
23+
`lockSource: 'package'` — ADR-0010's own reserved value for a lock the package
24+
layer asserts — plus the package id. `WRITABLE_PACKAGE_REQUIRED` would be the
25+
wrong prescription here: switching bases cannot help, because the artifact is
26+
code-shipped wherever the caller points. This is the server-side counterpart
27+
of the "Read-only" badge Studio already renders.
28+
- **`runtime-only`** (no artifact under this name — a NEW item authored into a
29+
read-only base) → `422 WRITABLE_PACKAGE_REQUIRED` with the package id, the
30+
same code, status and prescription `saveMetaItem` already emits for exactly
31+
this condition. One vocabulary, now stated at the single persistence route as
32+
well, so callers that do not pass through that gate cannot skip it.
33+
34+
**No allow decision moves.** Every write that succeeded before still succeeds:
35+
this is the code selection inside the refusal branch, not a new gate. That
36+
distinction is load-bearing rather than cautious — an ADR-0005 org overlay names
37+
the read-only package it customizes *by construction*, so a package door that
38+
refused would close the overlay model itself. Writes that name no base keep the
39+
previous `NOT_OVERRIDABLE` / `NOT_CREATABLE` codes verbatim, and the DELETE verb
40+
is unchanged (#6960 moved that side on purpose; `DeleteOptions` names no
41+
package).
42+
43+
The `OS_METADATA_WRITABLE` hatch is likewise untouched — structurally, because
44+
its limb returns before the new door — and is deliberately left **uncovered** by
45+
this change's tests, which the suite docblock records so the gap reads as a
46+
decision. The maintainer ruling of 2026-08-12 on #8146 holds that a hatch write
47+
into a read-only package should REFUSE, so a test of today's answer would be
48+
green *because the bug is present*; #8146 ships the refusal and its own
49+
rejection pin (`code` + `status`) together.
50+
51+
Re-measured on current `main` while this was in flight, and carried here because
52+
it is new evidence for that decision: the hatch write still succeeds, and the row
53+
lands bound **into** the read-only package (`package_id = com.example.showcase`,
54+
`organization_id = null`) rather than as the per-org override the variable's own
55+
documentation describes ("treats them as `allowOrgOverride: true`" — a
56+
*type*-level unlock, which says nothing about the package dimension).
57+
58+
Reachability, stated so it is not mistaken for more than it is: this refusal is
59+
what answers on the host-config topology (`environmentId` undefined — the CLI's
60+
lightweight assembler, i.e. the flagship showcase and self-hosted servers shaped
61+
like it), which is the topology the defect was measured on. On a scoped kernel
62+
`saveMetaItem` refuses earlier, in `protocol.ts`, still with the undiscriminated
63+
`NOT_OVERRIDABLE`; that second refusal point is filed separately.

0 commit comments

Comments
 (0)