Repository navigation
readonly-package-locks-studio: the read-only metadata refusal keys on the metadata type's overlay policy, not package writability (ITEM_LOCKED / WRITABLE_PACKAGE_REQUIRED never emitted) #7682
Description
Activity
- addedbugSomething isn't workingSomething isn't workingand removed
on Aug 11, 2026 huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsClaim: PM loop round 12
Session:session_01AmES43BMDg4bPrxTdi5q7t
Branch:claude/issue-7682-package-writability-refusal
Worktree:objectstack-issue-7682
Domain:domain:metadata
File surface:packages/metadata-protocol/src/sys-metadata-repository.ts(region:assertAllowedand the code-selection it feeds) + its tests. (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus
Serial constraints cleared: no in-flight card in this lane declaressys-metadata-repository.ts. Sibling cards onprotocol.ts(#8136, #8003) are a different file — do not editprotocol.ts.Scope ruling (PM, binding for this dispatch). This card carries two separable defects and only the first is dispatched:
- In scope — the invariant restoration.
ITEM_LOCKEDandWRITABLE_PACKAGE_REQUIREDare in the error-code ledger and are never emitted on this path; the refusal discriminates on the metadata type's overlay policy instead of package writability. This is declared ≠ enforced, and restoring it needs no maintainer call. - Out of scope — the
OS_METADATA_WRITABLEhatch vs the Studio "Read-only" badge. The card asks the maintainer to rule "intended hatch (fix the badge)" vs "badge is correct (server should refuse)". That is a product-semantics call spanning this repo andobjectui, and it is being filed separately rather than answered here.
Therefore: preserve the
OS_METADATA_WRITABLEbehaviour exactly as measured — an env-hatch write that succeeds today must still succeed after your change — and pin that behaviour so the separate decision can move it deliberately later. Changing it silently while fixing (1) would pre-empt the maintainer's ruling.Verified on
origin/mainbefore dispatch: the discriminator is live atpackages/metadata-protocol/src/sys-metadata-repository.ts:1086(intent === 'runtime-only' ? 'NOT_CREATABLE' : 'NOT_OVERRIDABLE'), matching the card's~L1086.
Generated by Claude Code
- In scope — the invariant restoration.
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsThe carved-out half of this card is now filed as #8146 — "
OS_METADATA_WRITABLEunlocks a write on a read-only package while Studio renders a 'Read-only' badge — which one is telling the truth?" — carryingneeds-user-decision+repo:objectui, with the three options (fix the badge / server should refuse / allow-but-loudly), a recommendation, and the four-cornered card face.For the dev on this card: nothing changes in your instructions. Preserve the
OS_METADATA_WRITABLEbehaviour exactly as measured and pin it. #8146 is where it moves, if it moves.
Generated by Claude Code
⚠️ In-flight scope change — read before landing any pin.Maintainer ruling (2026-08-12, live PM chat session
session_01GxKQfv3k8b6a2d2QrZU411, verbatim: 「接受你的全部建议。」 on the decision-box analysis) has settled #8146 as option B: the server should refuse — the badge is telling the truth. The hatch is type-level by its own shipped documentation (content/docs/deployment/environment-variables.mdx:305; v15 release notes distinguish the package-level vsOS_METADATA_WRITABLEtype-level gates), so the measured 200 on a read-only package is a bug, not intended behaviour.Consequence for this card's dispatch: ⛔ do not land a pin asserting the current hatch-bypasses-package-lock behaviour as correct. First re-reproduce the 200 on current main (the original measurement is from two-week-old builds): if it no longer reproduces, pin the refusal and close #8146 as moot; if it reproduces, the deliverable becomes the refusal + a rejection pin (
code+status), per #8146's ruling. If your in-flight work has already built the preserve-and-pin version, stop and report — don't merge it.
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actions{ "issue": 7682, "status": "done", "branch": "claude/issue-7682-package-writability-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/8185", "premise_still_valid": true, "summary": "SysMetadataRepository.assertAllowed now reads the base the caller named (PutOptions.packageId) through the shared isWritablePackage predicate, so a REFUSED write that named a read-only package says which door refused it: override-artifact -> 403 ITEM_LOCKED with ADR-0010's reserved lockSource:'package' and the package id; runtime-only -> 422 WRITABLE_PACKAGE_REQUIRED, the same code/status/prescription saveMetaItem's ADR-0070 D1 gate already emits, now stated at the single persistence route too. Both ledgered codes were previously never emitted on this path. Deliberately a code SELECTION inside the refusal branch, not a new gate: every allow limb returns first, so an ADR-0005 overlay (which names the read-only package it customizes by construction) and the OS_METADATA_WRITABLE hatch behave exactly as measured, both pinned by tests per the ruling; writes naming no base and the DELETE verb keep their codes verbatim. Both PM mechanism assumptions confirmed - the discriminator is live in assertAllowed, and the writability fact needed no new plumbing (packageId + this.engine + the existing package-writability.ts predicate are all already at the call site).", "tests": "New suite packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts, 10 cases, every refusal pinning BOTH code and status; the read-only/writable pair asserted as a difference. Reverse verification with the direction predicted first: reverting the source and keeping the suite gives '6 failed | 4 passed' - the 6 discrimination cases red, the 4 preservation pins (ADR-0005 overlay lands, hatch write succeeds, no-base codes, delete codes) green, proving they do not depend on this change. Restored: 'Test Files 1 passed | Tests 10 passed'. Suites (flock-serialised, --max-old-space-size=4096, --workspace-concurrency=2): metadata-protocol 76 files/1104 tests passed; objectql 191/3390 passed; rest 102/1728 passed; runtime 145/2195 passed. Consumer direction: those three downstream packages picked by hand as the ones exercising this refusal (the full '...' prefix set is effectively the whole repo); '^...' used only to build dependency closures - rest and runtime first failed to COLLECT on unbuilt sibling dists in a fresh worktree (stale-artefact trap, not this change) and are green after their closures were built. Gates: check:cross-package-test-inputs + check:durability-log-level (both named by scripts/pm/dispatch-gates.mjs for this diff), check:error-code-casing, check:nul-bytes plus a widened control-byte self-scan, spec error-code-ledger.test.ts (8 passed), ESLint on both files (clean), check-empty-changeset / check-changeset-no-major. Changeset added. No packages/spec change needed: both codes are already registered to @objectstack/metadata-protocol in the ledger.", "open_questions": [ { "question": "PR body opens 'Fixes #7682' per the dispatch instruction, but my standing contract says use 'Part of' when merging would not close the card - the card's own Suggested fix has two halves and the second (badge-vs-hatch) is excluded by scope and now lives in #8146. Surfacing the conflict rather than silently picking, as required. One word for the PM to flip either way.", "options": ["A keep 'Fixes #7682' - the PM extracted the decision half into #8146, so what remains of the card IS the invariant restoration this PR completes", "B switch to 'Part of #7682' and let #8146 (and #8184) close it later"], "recommendation": "A, because the PM's own scope ruling redefined #7682's remaining content as the invariant half and filed the rest as #8146; but this is bookkeeping the PM owns, and B costs one edit." }, { "question": "Which ledgered code the override-artifact refusal should carry was the one interpretive call in this fix. I chose ITEM_LOCKED (403, lockSource:'package') over WRITABLE_PACKAGE_REQUIRED because the latter prescribes 'pick a writable base', which is FALSE for an artifact-backed item - it is code-shipped wherever the caller points - and a Studio/AI client that auto-retried on it would fail again. ITEM_LOCKED prescribes nothing package-wise and matches the Read-only badge the card says Studio renders.", "options": ["A ITEM_LOCKED for override-artifact, WRITABLE_PACKAGE_REQUIRED for runtime-only (shipped)", "B WRITABLE_PACKAGE_REQUIRED for both, one code for one condition", "C keep NOT_OVERRIDABLE for override-artifact and emit only WRITABLE_PACKAGE_REQUIRED for runtime-only - but then the card's headline PUT stays undifferentiated between a read-only and a writable base, i.e. the defect survives"], "recommendation": "A. It is the only partition where each code's prescription is true, both ledgered codes get emitted where they apply as the card asks, the card's headline case keeps its 403, and no new vocabulary is minted. If a maintainer disagrees, B is a two-line change in the same function." } ], "out_of_scope_findings": [ "filed as #8184: saveMetaItem's scoped-kernel branch (environmentId !== undefined, protocol.ts) refuses artifact-backed writes BEFORE the repository door runs, so on a cloud per-env kernel the same request still answers the undiscriminated NOT_OVERRIDABLE while a host-config kernel now answers ITEM_LOCKED. Not a regression (that branch answered NOT_OVERRIDABLE before this PR too); this change makes the divergence visible. Not fixed here: protocol.ts was declared off-limits for this dispatch (#8136, #8003 are in that file concurrently). Searched open issues first - no twin; the new suite's final describe states the boundary so it is not mistaken for coverage." ] }Notes not carried by the JSON fields:
- Issue body truncation check: performed via the MCP path only (
issue_readget+get_comments), per the PM's mid-run correction — rawapi.github.comovercurlanswers 403 in this container (GitHub access is not enabled for this session), so the two-reader.body/.body_htmlcomparison was not available. The body ends in a complete## Sourcesection naming QA run QA run · access-security (FULL area) · 92f26f75 · 2026-08-11 · 8 PASS / 2 PARTIAL / 8 FAIL / 1 BLOCKED #7637 (framework92f26f75, console09987b680), with no truncation marker. - No
skip-changesetlabel: this PR ships a changeset (.changeset/metadata-refusal-package-writability.md), so the label does not apply. - File surface respected:
packages/metadata-protocol/src/sys-metadata-repository.ts+ one new test file in the same package + the changeset.protocol.tsuntouched — the one place the fix arguably wants to reach is filed as The metadata write refusal answers a different code per TOPOLOGY:saveMetaItem's scoped-kernel branch refuses before the repository's package door ever runs #8184 instead of crossed.
Generated by Claude Code
- Issue body truncation check: performed via the MCP path only (
- added a commit that references this issue
on Aug 12, 2026 huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsSupersedes my earlier report comment on this issue. That one was written before I saw the in-flight ruling comment of 18:21 (it landed after my initial read of this card, and I only saw it when reading my own comment back). Status changes from
donetoneeds_decision; the code deliverable is unchanged and complete, what changed is that one claim it was making is now repudiated and one fork needs a ruling.{ "issue": 7682, "status": "needs_decision", "branch": "claude/issue-7682-package-writability-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/8185", "premise_still_valid": true, "summary": "The in-scope invariant half is implemented and complete: SysMetadataRepository.assertAllowed now reads the base the caller named (PutOptions.packageId) through the shared isWritablePackage predicate, so a REFUSED write that named a read-only package says which door refused it - override-artifact -> 403 ITEM_LOCKED carrying ADR-0010's reserved lockSource:'package' and the package id; runtime-only -> 422 WRITABLE_PACKAGE_REQUIRED, the same code/status/prescription saveMetaItem's ADR-0070 D1 gate already emits. Both ledgered codes were previously never emitted on this path. It is a code SELECTION inside the refusal branch, not a new gate: every allow limb returns first, so no write that succeeded before is refused now. BUT the dispatch's ruling #2 (preserve and pin OS_METADATA_WRITABLE) was superseded mid-flight by the 2026-08-12 comment settling #8146 as option B, which says do not land a pin asserting that behaviour as correct and, for work that already built the preserve-and-pin version, 'stop and report - don't merge it'. This is that work. I did NOT implement the new direction unilaterally: it narrows a documented escape hatch and option B has two readings with very different blast radius (see open_questions). Instead I did what the ruling asked - re-reproduced on current main, and neutralised the claim: the hatch limb was never touched by this PR (structurally, it returns before the new door), so nothing needed reverting; the test case is relabelled '[CONTESTED - #8146 ruled this a BUG]' as a characterization pin of today's answer and kept deliberately as the tripwire the #8146 fix must invert, and the changeset now says the same. PR first line is 'Part of #7682', not 'Fixes'.", "tests": "RE-REPRODUCTION requested by the ruling (original measurement was two-week-old builds): it STILL REPRODUCES on current main, end to end through saveMetaItem on the host-config topology. OS_METADATA_WRITABLE=permission + packageId='com.example.showcase' (booted read-only code package) -> {success:true, seq:1, state:'active', message:'Saved customization overlay (env-wide, state=active) - type=permission, name=showcase_contributor'}, and the row lands {package_id:'com.example.showcase', organization_id:null} - i.e. bound INTO the read-only package, not the per-org override the variable's own docs describe. Probe was a temporary test file, run and deleted; not committed. Suite: packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts, 10 cases, every refusal pinning BOTH code and status, the read-only/writable pair asserted as a difference. Reverse verification with direction predicted first: reverting the source and keeping the suite gives '6 failed | 4 passed' - the 6 discrimination cases red, the 4 characterization/preservation cases green, proving they do not depend on this change. Restored: 10 passed. Package suites (flock-serialised, --max-old-space-size=4096, --workspace-concurrency=2): metadata-protocol 76 files/1104 tests; objectql 191/3390; rest 102/1728; runtime 145/2195 - all passed. Consumer direction: those three downstream packages picked by hand as the ones exercising this refusal (the full '...' prefix set is effectively the whole repo); '^...' used only for build closures - rest and runtime first failed to COLLECT on unbuilt sibling dists in a fresh worktree (stale-artefact trap, not this change) and are green after their closures were built. Gates: check:cross-package-test-inputs, check:durability-log-level (both named by scripts/pm/dispatch-gates.mjs for this diff), check:error-code-casing, check:nul-bytes plus a widened control-byte self-scan, spec error-code-ledger.test.ts, ESLint on both files, check-empty-changeset / check-changeset-no-major. All green.", "open_questions": [ { "question": "#8146 is ruled option B ('the server should refuse'), justified by the hatch being TYPE-level per its shipped documentation. But that same documentation is what makes option B ambiguous: OS_METADATA_WRITABLE is documented as 'treats them as allowOrgOverride: true, letting artifact-backed items of those protected types be overridden per-org' - and artifact-backed items only ever ship FROM a package, which is read-only by definition. So 'the package lock beats the hatch' can mean two very different things, and I will not pick between them: one keeps the hatch doing its documented job, the other stops it doing anything at all.", "options": [ "NARROW - refuse only when the caller NAMES a read-only base (?package=READONLY). A package-less hatch write still lands the env-wide/per-org overlay row the documentation promises. Matches the measurement exactly (the QA 200 wrote package_id=com.example.showcase, which the docs never describe), matches the Studio badge (rendered on the package-scoped matrix), and is ~3 lines: move the package door above the env-hatch limb but below the registry limb.", "BROAD - the hatch never unlocks a write against an item a read-only package provides, package named or not. Faithful to 'the badge is telling the truth' in the strongest sense, but it retires the hatch's only documented use (overriding artifact-backed items) - operators lose the emergency channel ADR-0005 grants them, and content/docs/deployment/environment-variables.mdx becomes wrong and must change in the same PR.", "MOOT-CHECK-FAILED - the ruling's third branch ('if it no longer reproduces, pin the refusal and close #8146 as moot') is not available: it does reproduce, measured above." ], "recommendation": "NARROW. It is the only reading under which the ruling's own justification (the hatch is type-level) and the hatch's shipped documentation are both true at once: a type-level unlock keeps unlocking the TYPE, and says nothing about the PACKAGE dimension - so naming a read-only base is refused while the per-org override survives. BROAD would need the docs page and ADR-0005's escape-hatch promise changed in the same breath, which is a bigger call than a refusal gate and is not something a dev should infer. Either way this is public-contract semantics, so it wants an explicit ruling rather than my guess - and either is small enough to be a same-lane follow-up on this branch or a fresh card." }, { "question": "Which ledgered code the override-artifact refusal should carry was the one interpretive call inside the in-scope half. I chose ITEM_LOCKED (403, lockSource:'package') over WRITABLE_PACKAGE_REQUIRED because the latter prescribes 'pick a writable base', which is FALSE for an artifact-backed item - it is code-shipped wherever the caller points - so a Studio/AI client that auto-retried on it would fail again.", "options": ["A ITEM_LOCKED for override-artifact, WRITABLE_PACKAGE_REQUIRED for runtime-only (shipped)", "B WRITABLE_PACKAGE_REQUIRED for both - one condition, one code", "C keep NOT_OVERRIDABLE for override-artifact and emit only WRITABLE_PACKAGE_REQUIRED for runtime-only - but then the card's headline PUT stays undifferentiated between a read-only and a writable base, i.e. the reported defect survives"], "recommendation": "A. It is the only partition where each code's prescription is true, both ledgered codes get emitted where they apply as the card asks, the card's headline case keeps its 403, and no new vocabulary is minted. Note it also lines up with the #8146 direction: ITEM_LOCKED is the server-side counterpart of the Read-only badge the ruling says is telling the truth. If a maintainer disagrees, B is a two-line change in the same function." } ], "out_of_scope_findings": [ "filed as #8184: saveMetaItem's scoped-kernel branch (environmentId !== undefined, protocol.ts) refuses artifact-backed writes BEFORE the repository door runs, so on a cloud per-env kernel the same request still answers the undiscriminated NOT_OVERRIDABLE while a host-config kernel now answers ITEM_LOCKED. Not a regression (that branch answered NOT_OVERRIDABLE before this PR too); this change makes the divergence visible. Not fixed here: protocol.ts was declared off-limits for this dispatch (#8136, #8003 are in that file concurrently). Searched open issues first - no twin; the new suite's final describe states the boundary so it is not mistaken for coverage." ] }Notes not carried by the JSON fields:
- Timing, stated plainly: the ruling comment (18:21) landed ~15 minutes after I started and was not present when I read this card's comments at the outset. I saw it at 19:00 when reading my own report comment back — which is the only reason it was caught before anything merged. Nothing was merged; the PR is and stays a draft.
- Instruction conflict, surfaced rather than resolved: the dispatch prompt's ruling ✨ Set up Copilot instructions #2 ("preserve
OS_METADATA_WRITABLEbehaviour exactly as measured, pin it") and the [Decision]OS_METADATA_WRITABLEunlocks a write on a read-only package while Studio renders a "Read-only" badge — which one is telling the truth? #8146 ruling ("do not land a pin asserting that behaviour as correct") are in direct opposition. I followed the newer one on the claim (relabelled, not asserted) and the older one on the code (the hatch path is untouched — which both rulings permit, since neither asks this PR to change hatch behaviour without the fork above being settled). - Issue body truncation check: via the MCP path only (
issue_readget+get_comments), per the PM's mid-run correction — rawapi.github.comovercurlanswers 403 here, so the.body/.body_htmltwo-reader comparison was not available. The body ends in a complete## Sourcesection naming QA run QA run · access-security (FULL area) · 92f26f75 · 2026-08-11 · 8 PASS / 2 PARTIAL / 8 FAIL / 1 BLOCKED #7637 (framework92f26f75, console09987b680). - No
skip-changesetlabel: this PR ships a changeset, so the label does not apply. - File surface respected:
sys-metadata-repository.ts+ one new test file in the same package + the changeset.protocol.tsuntouched — the one place the fix arguably wants to reach is filed as The metadata write refusal answers a different code per TOPOLOGY:saveMetaItem's scoped-kernel branch refuses before the repository's package door ever runs #8184 rather than crossed.
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsPM review of PR #8185 — the invariant half is ACCEPTED on its merits; one small patch round before it can land, and one fork goes to the maintainer.
First, the process point, because it is the most valuable thing here. My ruling #2 on this card was wrong by 20 minutes — I dispatched "preserve and pin the hatch" at 17:55Z, and the maintainer settled #8146 as option B at 18:21Z, inverting it. The dev caught the superseding comment at 19:00Z while reading its own report back, retracted a report it had already posted, and re-filed as
needs_decision. It would have been easy and invisible to let the earlierdonestand. ⭐ Retracting your own filed report is the behaviour that makes reports trustworthy at all.It also handled the instruction conflict exactly right: followed the newer ruling on the claim (relabelled rather than asserted) and the older one on the code (hatch path untouched), and surfaced the conflict instead of resolving it unilaterally. ⛔ It did not implement the new direction on its own — correct, because that direction narrows a documented escape hatch.
The code, verified against the diff
opts.packageId, nottargetPackageId— deliberately, and correct.undefinedmeans "the caller named no base" (the ordinary env-local overlay, which must keep the type-door codes);?? nullon the row-key path is a different fact. Getting this backwards would have re-coded every ordinary overlay refusal in the product, and the suite pins that it does not.- The door sits after every allow limb, so no allow decision moves. This is the load-bearing design call: an ADR-0005 overlay names the read-only package it customizes by construction, so a naive "refuse writes into a read-only package" gate would close the overlay model itself. There is a test asserting that overlay still lands. ⭐ That is the case that decides the shape of the fix, and it is present.
isWritablePackageimported, not re-spelled — the same predicatesaveMetaItem's ADR-0070 D1 gate and the/packageslifecycle gate use, so a future third read-only signal reaches this door too.- Both refusals pin
codeandstatus, the read-only/writable pair is asserted as a difference (with an explicitnot.toBe), read-only-by-scope and read-only-by-boot both covered, DELETE deliberately not symmetrised per 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. - Reverse verification with the direction predicted first: 6 discrimination cases red on revert, 4 preservation cases green — proving the preservation pins do not depend on the change.
Ruling on open question 2 — the code partition: A, as shipped
ITEM_LOCKEDforoverride-artifact,WRITABLE_PACKAGE_REQUIREDforruntime-only. The reasoning is right and is the deciding factor:WRITABLE_PACKAGE_REQUIREDprescribes "pick a writable base", which is false for an artifact-backed item — it is code-shipped wherever the caller points — so a Studio or AI client that auto-retried on it would fail again, and we would have replaced an uninformative refusal with a misleading one.ITEM_LOCKED+ ADR-0010's reservedlockSource: 'package'states the true fact and prescribes the two things that actually move it. Both ledgered codes get emitted where they apply, and no new vocabulary is minted.Ruling on open question 1 (
FixesvsPart of) —Part of #7682, as shippedCorrect, and my dispatch instruction was stale. #8146 and #8184 both carry remaining content, so merging must not close this card. ⇒ On merge I will re-grade this card in the same label write rather than let it drop out of view.
⚠️ Patch round — remove the contested characterization pinOne change before this lands. The
[CONTESTED — #8146 ruled this a BUG]case is a green test that passes because the bug exists. The 18:21Z instruction was explicit — "don't merge it" — and while relabelling neutralised the claim, the assertion still executes and still goes green off ruled-buggy behaviour. I am not going to reinterpret an explicit instruction into "merge it anyway with a better comment".The tripwire argument is good in general — this lane does use deliberately-red-in-future pins — but it is redundant here: #8146's ruling already mandates "refusal + rejection pin asserting
codeandstatus" as its deliverable, so nothing will be forgotten. And this lane has already been bitten by the other failure mode, where a deliberately-red pin gets "repaired" to green by someone who did not read why it was there.⇒ Drop that one case. Keep everything else, including "without the hatch, that same permission write is refused by the package door" — that one asserts correctness and is the point of the card. Add a line to the suite docblock saying the hatch path is deliberately uncovered pending #8146, so the absence reads as a decision rather than an oversight. I am recording the same on #8146 so its implementer knows the pin is theirs to write.
Everything else stands. Once that lands and CI concludes green on its own job conclusions, this goes ready + auto-merge.
Out-of-scope finding
#8184 accepted as filed — the scoped-kernel branch refusing earlier in
protocol.tsmeans a cloud per-env kernel still answers the undiscriminatedNOT_OVERRIDABLE. ⭐ Correctly not fixed here:protocol.tswas off-limits with #8136 and #8003 concurrent in it, and the suite's finaldescribestates the boundary so it is not mistaken for coverage. That is the right way to decline a crossing.
Generated by Claude Code
- added 3 commits that reference this issue
on Aug 13, 2026 Closure action for
Part ofPR #8185 — mergedebf7d98onorigin/main(verified by commit, ⛔ not byauto_merge).Executed by the incoming
domain:metadataseat PM (session_012WMpuAfA2KSdDjGF6tm1bH); the action was left pending when the previous shift closed with the PR still in the merge queue.Delivered
The card's primary defect is fixed.
SysMetadataRepository.assertAllowednow reads the base the caller actually named and emits the two codes the error-code ledger already registered to@objectstack/metadata-protocol:override-artifacton a read-only base →403 ITEM_LOCKED(lockSource: 'package'+ package id) —WRITABLE_PACKAGE_REQUIREDwould be a false prescription, since a code-shipped artifact is locked wherever the caller points.runtime-onlyon a read-only base →422 WRITABLE_PACKAGE_REQUIRED+ package id, matching whatsaveMetaItemalready emits for that condition (ADR-0070 D1).
No allow decision changed — every allow limb returns before the new door, so ADR-0005 org overlays and
promoteDraft/restoreVersion/revertCommitare untouched and pinned as such. 9 new cases insys-metadata-repository.package-writability.test.ts, each pinning bothcodeandstatus, reverse-verified with the preservation cases green by design.Remaining — neither item is left untracked, and neither belongs on this card
remainder carried by state The OS_METADATA_WRITABLEhatch writes while Studio renders a "Read-only" badge — the caveat this card raised#8146 ruled option B; the remaining NARROW/BROAD fork is in the maintainer decision box under an explicit veto window saveMetaItem's scoped-kernel branch (environmentId !== undefined) refuses before the repository door runs, so a per-env cloud kernel still answers the undiscriminatedNOT_OVERRIDABLE#8184 pm:queue,domain:metadata— dispatchableNot a regression in either case: that branch answered
NOT_OVERRIDABLEbefore #8185 too; the fix makes the divergence visible. The new suite's finaldescribestates the boundary so it does not read as coverage.Grading
Closing as completed rather than returning to
pm:queue. Nothing dispatchable remains on this card — its entire residue is #8146 and #8184, both open. Re-queueing it would put a card back in the pool whose whole content is tracked twice elsewhere, and a future dispatch would be spent rediscovering that.Owner of the residue: this seat (
domain:metadata) for #8184; the maintainer for #8146's fork.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Symptom
Studio's read-only lock is real, not cosmetic (207/207 checkboxes disabled, 92/92 bulk buttons disabled, no Save rendered, Publish disabled; a forced click left
data-stateunchanged; the direct write is refused and the artifact is byte-identical afterwards, sha256 match). But the server-side refusal does not key on package writability:PUT /api/v1/meta/object/showcase_task→ 403NOT_OVERRIDABLE("'object' is not allowOrgOverride in the registry"), the same with?package=pointing at either a read-only or a writable package.ITEM_LOCKEDandWRITABLE_PACKAGE_REQUIREDare both in the error-code ledger and neither was ever emitted on this path.Root cause
Located by the run.
SysMetadataRepository.assertAllowed(packages/metadata-protocol/src/sys-metadata-repository.ts~L1055; code selected at ~L1086:intent === 'runtime-only' ? 'NOT_CREATABLE' : 'NOT_OVERRIDABLE') discriminates on the metadata type's overlay policy (allowOrgOverridein the registry), not on a package-writability check. Confirmed present onorigin/main.Caveat needing a maintainer decision
With the documented operator hatch
OS_METADATA_WRITABLE=permission,PUT /api/v1/meta/permission/showcase_contributor?package=com.example.showcase— a set belonging to the read-only package — succeeds (200, env-wide overlay), while Studio still renders that matrix fully disabled with a "Read-only" badge. The hatch is documented as unlocking the write, so this may be intended — but the Studio badge then asserts a lock the server is not applying. Please rule: intended hatch (fix the badge) vs badge is correct (the server should refuse). (The overlay was reverted afterwards.)Reproduction
PUT /api/v1/meta/object/showcase_task→ 403NOT_OVERRIDABLE; repeat with?package=<read-only pkg>and?package=<writable pkg>— same code, neverITEM_LOCKED/WRITABLE_PACKAGE_REQUIRED.OS_METADATA_WRITABLE=permission;PUT /api/v1/meta/permission/showcase_contributor?package=com.example.showcase→ 200 while Studio still shows the matrix disabled with a "Read-only" badge.Suggested fix
Make the refusal on the package door discriminate on package writability (emitting the ledgered
ITEM_LOCKED/WRITABLE_PACKAGE_REQUIREDwhere appropriate), and resolve the badge-vs-hatch inconsistency per the decision above.Source
Extracted from the QA run #7637 (framework 92f26f7, console 09987b680).