You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
Observation-class finding, spotted while writing the ADR-0094 D5-R revision (#6858). Dormant today — the in-repo corpus has zero such rows (measured at PR #6608) — so nothing is user-visible right now. Filing plainly and leaving the grading to triage.
The asymmetry
#6483 / PR #6608 flipped ten types to allowOrgOverride: false. That closed the write door. It did not close the read path: supportsOverlay: true is unchanged for permission / position / page / app / dataset / book, so an overlay row authored before the rollback still merges overlay-wins at read time and still shapes the effective body.
Removing such a row is where it gets stuck. deleteMetaItem refuses an artifact-backed target of a non-overridable type on both paths, and neither is conditioned on an overlay row actually existing in a way that helps:
packages/metadata-protocol/src/protocol.ts — inside environmentId !== undefined, artifactBacked && !overlayAllowed throws NOT_OVERRIDABLE / 403 before any probe for the row;
Net: on an environment carrying a pre-rollback overlay of an artifact-backed item, the row keeps applying and the ordinary "Reset to package default" flow answers 403. The only documented removal is the operator escape hatch, OS_METADATA_WRITABLE=(type).
Why it reads as unintended rather than ruled
The save-side gate carries a comment stating that deleteMetaItem is deliberately not gated the same way, with the reason that "removing a code-only row that predates this refusal is repair, and must stay possible". That reasoning applies verbatim to this case — a legacy overlay predating the rollback is exactly repair — but the carve-out as written only covers the code-only tier (allowRuntimeCreate: false AND allowOrgOverride: false), not the artifact-backed tier a rolled-back type now sits in. So the stated intent and the enforced behaviour diverge for this one shape.
Not asserting which side should move: a delete that lifts an overlay is strictly de-escalating on the authorization surface, which argues for allowing it; equally, "any write against an artifact-backed authorization item is refused" is a clean, defensible line that an operator hatch already relieves. That is a call for triage / the maintainer, not for this finding.
Blast radius
Zero in-repo today. It bites an environment that upgrades across the #6483 rollback while holding such a row, and the symptom is a Reset button that 403s with the item still customized. Scope is the metadata protocol and all rolled-back overlayable types, not ADR-0094 — which is why #6858 recorded the carry-over in ADR-0094 D5-R prose and filed this separately rather than widening its own change.
Refs: #6483, PR #6608, #6609 (ruling A), #6858 (the ADR-0094 revision that surfaced it), ADR-0005.
Triage (maintainer-authorized one-off pass, 2026-08-09, registered on #6015): held as finding, routed domain:metadata (was unrouted). Dormant — zero such overlay rows in the corpus (measured at PR #6608), so no stuck operator exists today. Restart conditions: (1) any deployment reports a legacy overlay row it cannot remove (the 403 path here), or (2) #6858's ADR-0094 D5-R revision lands — its ruling on the materialized-layer direction decides whether this delete path gains a legal door or the whole legacy-overlay layer gets an explicit migration story; re-grade then.
Maintainer ruling (2026-08-10, directed in session session_01BPWqbmEFU8gJepBJTHESXd): the delete side moves — removal of a legacy env overlay on a rolled-back type is allowed through the ordinary delete path.
Deleting an overlay restores the code-declared state: it is the narrowing direction and cannot widen anything, so refusing it serves no security purpose while trapping repair behind OS_METADATA_WRITABLE. Create and update on such items stay refused exactly as today. Record the asymmetry (delete-only) in the gate's doc so it is not "fixed" into symmetry later. Zero affected rows today — dispatch at normal priority, not urgent.
PM: ruling consumed, card is dispatchable — but it is SEQUENCED behind PR #7306, and that is a same-function conflict, not a formality (domain:metadata seat, session session_01W6bLax4KMrSfnE1ydFU8Dw). Recording now so it is not re-derived, and so nobody dispatches it in parallel.
The collision
The ruling moves the delete side: removal of a legacy env overlay on a rolled-back type goes through the ordinary delete path. Per this card's own analysis the refusal lives at exactly two sites:
SysMetadataRepository.assertAllowed, refusing override-artifact on the no-environmentId kernel path.
PR #7306 (ADR-0029 D9, Fixes #7277) just added pins to both of those sites, asserting the refusal. From its own report: "the same predicate feeds deleteMetaItem's two-tier authorization and SysMetadataRepository.assertAllowed, so RESETTING such a customization is refused too — and the repository check is topology-independent, so a control-plane kernel refuses as well. Both are pinned rather than softened (a fixture encoding the old leniency would be encoding the defect)."
So this card and that PR reach for the same two gates from opposite directions, in the same batch window. The maintainer's standing rule for this lane is explicit — protocol.ts region-disjoint parallel authorship is authorized, same function / same region never in the same batch. This is the same function. It waits.
The part that needs a ruling, and it is NOT a contradiction
The two are not actually in conflict on substance, and I want that on the record before anyone reads #7306's pins as blocking this ruling:
This card is the rolled-back-overlayable-type case — permission / position / page / app / dataset / book, where the maintainer has now ruled the delete direction open because it restores code-declared state and cannot widen anything.
Same argument shape, opposite outcomes, different type sets — which is coherent only if the implementation scopes the relaxation to the rolled-back overlayable types and leaves the object tier refusing exactly as D9 pins it. That is the design constraint whoever takes this card inherits, and it is the thing most likely to be got wrong by a dev who finds one predicate serving both and relaxes it.
The dispatch will carry, as a hard constraint: the object tier's refusal does not move; if the two cases cannot be separated without relaxing D9's pinned behaviour, STOP and report rather than editing those pins.
The delete-only asymmetry gets recorded in the gate's doc comment per the ruling, so it is not later "fixed" into symmetry — and the save-side comment that already promises repair-must-stay-possible gets extended to name the artifact-backed tier, since that divergence between stated intent and enforced behaviour is what this card opened with.
Zero affected rows today (measured at PR #6608), and the ruling itself says normal priority, not urgent — so the sequencing costs nothing real.
Session: session_01W6bLax4KMrSfnE1ydFU8Dw
Branch: claude/issue-6960-legacy-overlay-delete
Worktree: objectstack-issue-6960
Base: origin/main @ f16e54e1d or later
The gate this card was held behind has cleared. PR #7306 (ADR-0029 D9) landed at 09:03:31Z as f16e54e1d — verified by unbounded grep on the subject shape plus a single-parent check. The same-function conflict recorded above is therefore resolved in the only way it safely could be: D9's pins are now on main, so the dev branches from a tree that already contains them and any relaxation that disturbs them turns red immediately rather than at landing.
Dispatching now with the maintainer's 07:36Z ruling as the specification, and the constraint below as the hard boundary.
The boundary, restated because it is the thing most likely to be got wrong
Two tiers reach the same two refusal points, and only one of them moves:
delete is ALLOWED through the ordinary path — removing an overlay restores code-declared state, is strictly narrowing, and cannot widen anything
this card, 07:36Z
object (overlay write over a packaged object)
delete stays REFUSED — D9.6's declared cost, now pinned three times over asserting code AND status
ADR-0029 D9, landed f16e54e1d
A dev who finds one predicate serving both tiers and relaxes it will turn D9's pins red. Those pins are correct. If the two tiers cannot be separated without editing them, that is a STOP and report, not a judgement call.
The two refusal points, from this card's own analysis:
deleteMetaItem's environmentId !== undefined branch in packages/metadata-protocol/src/protocol.ts — artifactBacked && !overlayAllowed throws NOT_OVERRIDABLE / 403 before any probe for the row;
Record the delete-only asymmetry in the gate's doc comment, so it is not later "fixed" into symmetry by someone tidying up. The save-side gate already carries a comment promising that removing a legacy row is repair and must stay possible — but it scopes that carve-out to the code-only tier (allowRuntimeCreate: false AND allowOrgOverride: false), not the artifact-backed tier a rolled-back type now sits in. That divergence between stated intent and enforced behaviour is what this card opened with, so extending that comment to name the artifact-backed tier is part of the deliverable, not a nicety.
Create and update stay refused exactly as today. Only delete moves.
Pricing
Zero affected rows in the corpus today (measured at PR #6608), and the ruling says normal priority, not urgent. So this is a correctness-of-contract change with no live victim — which means the pins are the deliverable and a "it works now" demo is not evidence.
Batch occupancy at dispatch: #7369 in dev, #4717 dispatched alongside this one (region-disjoint — deleteMetaItem vs saveMetaItem). Three of five.
Activity
os-project-manager commentedon Aug 9, 2026
Triage (maintainer-authorized one-off pass, 2026-08-09, registered on #6015): held as
finding, routeddomain:metadata(was unrouted). Dormant — zero such overlay rows in the corpus (measured at PR #6608), so no stuck operator exists today. Restart conditions: (1) any deployment reports a legacy overlay row it cannot remove (the 403 path here), or (2) #6858's ADR-0094 D5-R revision lands — its ruling on the materialized-layer direction decides whether this delete path gains a legal door or the whole legacy-overlay layer gets an explicit migration story; re-grade then.Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Maintainer ruling (2026-08-10, directed in session
session_01BPWqbmEFU8gJepBJTHESXd): the delete side moves — removal of a legacy env overlay on a rolled-back type is allowed through the ordinary delete path.Deleting an overlay restores the code-declared state: it is the narrowing direction and cannot widen anything, so refusing it serves no security purpose while trapping repair behind
OS_METADATA_WRITABLE. Create and update on such items stay refused exactly as today. Record the asymmetry (delete-only) in the gate's doc so it is not "fixed" into symmetry later. Zero affected rows today — dispatch at normal priority, not urgent.needs-user-decision→pm:queue.Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
PM: ruling consumed, card is dispatchable — but it is SEQUENCED behind PR #7306, and that is a same-function conflict, not a formality (
domain:metadataseat, sessionsession_01W6bLax4KMrSfnE1ydFU8Dw). Recording now so it is not re-derived, and so nobody dispatches it in parallel.The collision
The ruling moves the delete side: removal of a legacy env overlay on a rolled-back type goes through the ordinary delete path. Per this card's own analysis the refusal lives at exactly two sites:
deleteMetaItem'senvironmentId !== undefinedbranch inpackages/metadata-protocol/src/protocol.ts(artifactBacked && !overlayAllowed→NOT_OVERRIDABLE/ 403);SysMetadataRepository.assertAllowed, refusingoverride-artifacton the no-environmentIdkernel path.PR #7306 (ADR-0029 D9,
Fixes #7277) just added pins to both of those sites, asserting the refusal. From its own report: "the same predicate feedsdeleteMetaItem's two-tier authorization andSysMetadataRepository.assertAllowed, so RESETTING such a customization is refused too — and the repository check is topology-independent, so a control-plane kernel refuses as well. Both are pinned rather than softened (a fixture encoding the old leniency would be encoding the defect)."So this card and that PR reach for the same two gates from opposite directions, in the same batch window. The maintainer's standing rule for this lane is explicit —
protocol.tsregion-disjoint parallel authorship is authorized, same function / same region never in the same batch. This is the same function. It waits.The part that needs a ruling, and it is NOT a contradiction
The two are not actually in conflict on substance, and I want that on the record before anyone reads #7306's pins as blocking this ruling:
objectcase — an overlay write over a packaged object, where the refusal is the ratified design and the reset refusal is its declared (and flagged) cost.permission/position/page/app/dataset/book, where the maintainer has now ruled the delete direction open because it restores code-declared state and cannot widen anything.Same argument shape, opposite outcomes, different type sets — which is coherent only if the implementation scopes the relaxation to the rolled-back overlayable types and leaves the
objecttier refusing exactly as D9 pins it. That is the design constraint whoever takes this card inherits, and it is the thing most likely to be got wrong by a dev who finds one predicate serving both and relaxes it.Dispatch conditions
ADR maintainer approval). Dispatching frommainbefore then produces a branch with neither D9 nor its pins, and the collision surfaces at landing instead of at authoring.objecttier's refusal does not move; if the two cases cannot be separated without relaxing D9's pinned behaviour, STOP and report rather than editing those pins.Zero affected rows today (measured at PR #6608), and the ruling itself says normal priority, not urgent — so the sequencing costs nothing real.
Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Claim: PM loop round 13 (
domain:metadataseat, sticker #6367)Session:
session_01W6bLax4KMrSfnE1ydFU8DwBranch:
claude/issue-6960-legacy-overlay-deleteWorktree:
objectstack-issue-6960Base:
origin/main@f16e54e1dor laterThe gate this card was held behind has cleared. PR #7306 (ADR-0029 D9) landed at 09:03:31Z as
f16e54e1d— verified by unbounded grep on the subject shape plus a single-parent check. The same-function conflict recorded above is therefore resolved in the only way it safely could be: D9's pins are now onmain, so the dev branches from a tree that already contains them and any relaxation that disturbs them turns red immediately rather than at landing.Dispatching now with the maintainer's 07:36Z ruling as the specification, and the constraint below as the hard boundary.
The boundary, restated because it is the thing most likely to be got wrong
Two tiers reach the same two refusal points, and only one of them moves:
permission/position/page/app/dataset/book)object(overlay write over a packaged object)codeANDstatusf16e54e1dA dev who finds one predicate serving both tiers and relaxes it will turn D9's pins red. Those pins are correct. If the two tiers cannot be separated without editing them, that is a STOP and report, not a judgement call.
The two refusal points, from this card's own analysis:
deleteMetaItem'senvironmentId !== undefinedbranch inpackages/metadata-protocol/src/protocol.ts—artifactBacked && !overlayAllowedthrowsNOT_OVERRIDABLE/ 403 before any probe for the row;SysMetadataRepository.assertAllowedrefusingoverride-artifacton the no-environmentIdkernel path (metadata: allowRuntimeCreate:false is not enforced — PUT /meta creates job and agent items the registry declares code-only #5086), reached exactly when a row does exist because the "no overlay found" probe returns a no-op success earlier.Also ruled, and easy to drop
Record the delete-only asymmetry in the gate's doc comment, so it is not later "fixed" into symmetry by someone tidying up. The save-side gate already carries a comment promising that removing a legacy row is repair and must stay possible — but it scopes that carve-out to the code-only tier (
allowRuntimeCreate: falseANDallowOrgOverride: false), not the artifact-backed tier a rolled-back type now sits in. That divergence between stated intent and enforced behaviour is what this card opened with, so extending that comment to name the artifact-backed tier is part of the deliverable, not a nicety.Create and update stay refused exactly as today. Only delete moves.
Pricing
Zero affected rows in the corpus today (measured at PR #6608), and the ruling says normal priority, not urgent. So this is a correctness-of-contract change with no live victim — which means the pins are the deliverable and a "it works now" demo is not evidence.
Batch occupancy at dispatch: #7369 in dev, #4717 dispatched alongside this one (region-disjoint —
deleteMetaItemvssaveMetaItem). Three of five.Generated by Claude Code
docs(plugin-security): restate the upsertEnvPermissionSet JSDoc for A…
fix(metadata-protocol): allow REMOVING a legacy env overlay on a roll…
fix(metadata-protocol): deleteMetaItem's catch re-wrap carries the er…
fix(metadata-protocol): the metadata write refusal reports the packag…
fix(runtime,metadata-protocol): the /automation write doors keep the …
fix(metadata-protocol)!: the ADR-0010 _lock gate refuses on a host-co…
fix(metadata-protocol)!: the metadata door refuses an edit of a code-…
test(metadata-protocol,rest,runtime,objectql,dogfood): pin the manage…
docs(permissions): describe the packaged permission-set lock in "One …