Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): OS_METADATA_WRITABLE does not unlock a write into a read-only package - #8320
Conversation
…fusal and its preservation cases (#8146) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
…d' comment #8185 wrote it while the OS_METADATA_WRITABLE gap was still open; this change closes that gap, so the comment now contradicts the suite docblock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8146
OS_METADATA_WRITABLEno longer unlocks a write into a read-only package. The operator hatch stays exactly as documented — a metadata-type-level unlock — and stops reaching the package dimension it was never described as touching.The defect
With the documented hatch set, the QA run measured:
com.example.showcaseis a read-only package, and Studio renders that same permission matrix fully disabled behind a "Read-only" badge. Two surfaces answered the same question differently — and the row landed bound into the read-only package, which is not the per-org overlay the variable's own documentation describes.Ruling chain (not re-litigated here)
content/docs/deployment/environment-variables.mdx: named types are treated "asallowOrgOverride: true… overridden per-org") and this package's CHANGELOG ("deliberately does not unlock the org dimension"). A type-level unlock says nothing about the package dimension, so the 200 is a bug.Premise verification — the measurement this card hangs on
NARROW is only justified if it preserves something: that a package-less hatch write currently lands a genuine env-wide / per-org overlay. That was an assumption, so it was measured against unpatched
mainrather than argued.Both preservation cases pass on
origin/mainwith the source change reverted — i.e. this behaviour exists today and this PR does not create it:So the hatch's documented behaviour is real, and NARROW genuinely preserves it. The card's reproduction also still reproduces on current
main(the refusal cases go red when reverted, meaning the write still answers 200) — the ruling's "close as moot" branch is not available.The fix
The whole change is limb ordering in
SysMetadataRepository.assertAllowed: the package door moves above the env-hatch limb and below every registry limb.isWritablePackageis reused frompackage-writability.ts, never re-spelled.A hatch write that names a read-only base is refused with the codes the error-code ledger already registers for this condition — both
codeandstatuspinned:override-artifact→403 ITEM_LOCKED, carryinglockSource: 'package'(ADR-0010's reserved value for a package-layer lock) and the package id — the server-side counterpart of Studio's badge.runtime-only→422 WRITABLE_PACKAGE_REQUIRED, the same code and prescriptionsaveMetaItemalready emits for ADR-0070 D1.What deliberately keeps working
The hatch is narrowed, not retired. No documentation changes and no capability is withdrawn:
The refusal no longer prescribes the step the caller already took
With the hatch open, the old message would have said "set
OS_METADATA_WRITABLE" while that variable is set — a false prescription of the kind #8185's patch round rejected, and the shape that makes an automated client retry forever. The message is now chosen by whether the hatch is open: with it open it explains that the hatch unlocks the type and not package writability and points at the remedy that works (retry without?package=); with it closed the original prescription is still offered, because then it is true. Samecodeandstatuseither way — one condition, only the remedy differs.Reverse verification (direction predicted before running)
Predicted: the discrimination cases go red, the preservation cases stay green. Measured exactly that — source reverted to
origin/main, suite kept:The 5 red are precisely the
#8146refusal cases (including the end-to-endsaveMetaItemreproduction). The 15 green include every preservation case — and their staying green on both sides is the point: they do not depend on this change, which is what makes them honest pins rather than artifacts of it.origin/mainhas not touchedsys-metadata-repository.tssince this branch's merge base, so the reverted file is genuinely the pre-change version.Tests
packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts— 20 cases (9 inherited from #8185, 11 added here). Every refusal case pins bothcodeandstatus; the preservation cases assert where the row binds, not merely that the call succeeded.Consumer sweep direction, stated so it can be reviewed: those three are downstream consumers, hand-picked as the packages that actually reference
OS_METADATA_WRITABLE/envWritableMetadataTypes(the full...@objectstack/metadata-protocolprefix set is effectively the whole repo). The^...suffix form was used only to build dependency closures. This change moves a real allow decision — unlike #8185, which moved only code selection — so the sweep was the point, not a formality.Gates
Named by the dispatch:
check:cross-package-test-inputs,check:durability-log-level,check:filter-alias-parity.Re-derived against the actual diff, which surfaced five families the dispatch list missed — all run, all green:
check:changeset-gate-self-tests,check:objectui-changeset,check:objectui-pin-fresh,check-changeset-no-major.mjs,check-objectui-pin-fresh.mjs. Pluscheck:nul-bytes,check:error-code-casing, and the two convention-triggered families a new test file moves:check:query-options-erasureandcheck:type-check-coverage(the type-check ledger was not raised — no new debt). ESLint clean on both files; the one complaint at line 863 is pre-existing and identical on the merge base.No new error codes are introduced — both were already registered to this package by #8185 — so nothing in
packages/specmoves.Known boundary — #8184 stays open, out of scope here
On a scoped kernel (
environmentId !== undefined)saveMetaItemrefuses earlier, inprotocol.ts, with the undiscriminatedNOT_OVERRIDABLE— so this refusal is not reachable on that topology. That file is forbidden to this card (#8136 and #7654 are editing it concurrently, and the scoped-kernel refusal there belongs to #8184). Not a regression: that branch answeredNOT_OVERRIDABLEbefore this change too. The suite's finaldescribestates the boundary so it does not read as coverage.Provenance note for the reviewer
This card was re-dispatched after an earlier agent's container died. That agent left no remote branch, but it had left a local commit and worktree, which this session recovered rather than rewrote. Everything above — the premise measurement, the reverse verification, the consumer sweep and every gate — was re-run in this session against the recovered tree; none of it is inherited on trust. One stale comment from #8185 ("the hatch half is deliberately uncovered") was corrected, since this change is what closes that gap.
Generated by Claude Code