Repository navigation
feat(plugin-security,rest,spec)!: retire the permission-set overlay discard (ADR-0131 cutover stage 7-pre) - #22776
Conversation
…iscard (ADR-0131 cutover stage 7-pre) U1 measured on main: no caller-reachable door creates an environment overlay of a package-declared permission set any more (the metadata door seals it on every topology, the data door's packaged lock refuses it, the data API cannot write sys_metadata, and a package declaring a held name is refused at install), and a database still holding one is refused at cold boot (ADR-0048). The discard action therefore had nothing it could reach on a running server, and the row it re-projected is no longer read for grants. Removes the action module and its tests, the REST route and its ledger row, the Setup action and its translations, the optional ISecurityService member and its result type, and plugin-security's two error-code provenance rows (both codes stay registered under @objectstack/rest). Registers the retirement as ADR-0087 semantic entry security-permission-set-overlay-discard-retired, and points every remaining reference (docs, the boot reading, the drift diagnostic) at the offline `os migrate security-catalog-overlays` step. Claude-Session: https://claude.ai/code/session_019TtY6pnoZSemcQRzbRUm5e Co-authored-by: Claude <noreply@anthropic.com>
…pre-overlay-discard # Conflicts: # packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts
📓 Docs Drift CheckThis PR changes 4 package(s): 34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 145 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 349cdd4216b7e1e09e7dca8d606c63d2cb950dbe && git checkout 349cdd4216b7e1e09e7dca8d606c63d2cb950dbe
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5fc57b382e0eb7357af1594628e6e44e205f2d7a 11afbdb847a0e5c9d68f7c75ac72381cce76c0a3 && git checkout -B drift-repro 5fc57b382e0eb7357af1594628e6e44e205f2d7a && git merge --no-ff 11afbdb847a0e5c9d68f7c75ac72381cce76c0a3
node scripts/docs-audit/affected-docs.mjs --json 5fc57b382e0eb7357af1594628e6e44e205f2d7a
|
…oute out The route-ledger row and the rest-server registration site left with the route: the matrix docblock reads 81 rest rows, and the blind-spot census re-measures 82 -> 81 (rest-route-ledger.ts) and 71/19/52 -> 70/19/51 with enforceAuth 58 -> 57 (rest-server.ts), totals 66/71 -> 65/70. Claude-Session: https://claude.ai/code/session_019TtY6pnoZSemcQRzbRUm5e Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #15204 (body and all 79 comments, in particular 6095755866 U1, 6105462924, 6106681412, 6105972564, 6106242585, 6106999102), PR #22776 (body, 47-file list, net diff +228/−1,587 against ① Derived judgmentsU1 → RETIRE is justified by measurement. The decision rule of 6105462924 asks one question: after the cutover, can any caller-reachable path still create an environment overlay of a packaged permission set? Re-measured on
One over-statement, recorded, not a FAIL: the cold-boot refusal reads the registry's bare slot, which hydration fills only when Accept-set and public-surface changes the diff implies, each named:
No accept set widens anywhere in the diff. A note for 6b-2, not a defect here: the ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…pre-overlay-discard
…tree Claude-Session: https://claude.ai/code/session_019TtY6pnoZSemcQRzbRUm5e Co-authored-by: Claude <noreply@anthropic.com>
7-pre (#22776): the regen-merge round is checked; queued · epic PM
|
Part of #15204
Clause-②: no (narrowing)
Stage 7-pre of the ADR-0131 cutover (claim amendment 6105462924; stage 0 item U1 of 6095755866). #15204 stays open: this is one stage of its plan, and the later stages (6b-2, 7a, 7b, 8) are not addressed here.
U1, measured on
maina18c514965(re-read after merging490cb6d9fa): the action retires1. What an "overlay" is now. An active, environment-wide
sys_metadatarow (organization_idnull) of typepermissionor the legacypermissions, stored under the name of a permission set a code package ships (classifyPackagedPermissionSetverdictpackaged). It is not asys_permission_setrow. The removed module found it withfindActiveOverlayRowsand gated it on that classifier.2. Can any caller-reachable path still create one? No. Every door was measured:
saveMetaItemasksrefusePackagedBaseOverride, which refuses an in-place write over a packaged item of a type with no overlay channel (isSealedManagedItem).permissionis registeredallowOrgOverride: false. ADR-0131 D6: theOS_METADATA_WRITABLEhatch "is not consulted" for managed content, on every topology.SysMetadataRepository.assertAllowedrepeats the refusal at the store (packages/metadata-protocol/src/protocol.ts,packagedBaseRefusaland its header).sys_metadata. The object isapiMethods: ['get', 'list'](packages/metadata-core/src/objects/sys-metadata.object.ts), so it is read-only over the data API.sys_permission_setand Setup.packaged-permission-set-lock.tsrefuses a save over a packaged set with 403NOT_OVERRIDABLE, naming Clone ("No silent overlay row is ever minted again"). The metadata-door half of that lock ispackaged-permission-set-lock-gate.ts.NAMESPACE_CONFLICT, on a hot install and on a cold boot (refuseSecurityCatalogNameConflicts;BUILT_IN_SECURITY_CATALOG_NAMES.permissionis empty, so no permission-set name is exempt).sys_metadatahydrates:ObjectQLPlugin.refuseEnvironmentHeldSecurityCatalogNamesreads the registry'senvironmentHeldSecurityCatalogConflicts(ADR-0048 N.3, registered assecurity-catalog-environment-overlay-refused; pinned bypackages/runtime/src/standalone-stack-security-catalog-one-holder.test.ts, "artifact boot over one database …"). So a server that could answer the discard route never holds an overlay. The remedy that exists for those rows is the offlineos migrate security-catalog-overlays [--apply](maintainer ruling letter B on [decision] after #22307 a cold boot refuses any environment row over a package-held permission set or position, so the 2026-08-24 legacy-overlay remedies (boot overlay reading, drift overlay_shadow, Discard Overlay) find nothing on v18: keep or retire? #22371).3. What the discard did, and whether the resolver reads it. It deleted the overlay row(s) under
SYSTEM_CTX, then re-projected thesys_permission_setrow throughprojectPermissionMutation. An overlay row would be read: hydration puts it in the registry's bare slot, which the security catalog reader serves. But no running server holds one (point 2). The re-projected row is not read for grants: since stage 1 the resolver reads the security catalog, and since stage 2a/2b the services readers do too.Decision rule → RETIRE. No path creates an overlay. The operator workflow for existing databases is already served by the offline step, so no product choice is left open.
Four axes (
.claude/skills/pm-dispatch/SKILL.md, 升级与决策):os migrate security-catalog-overlays, which runs where the action cannot: before boot.What changed
plugin-security/src/permission-set-overlay-discard.tsand its test, and the dogfood eligibility test.sys_permission_setgoes (translations regenerated). Thedrift_statusfield description, the drift diagnostic'soverlay_shadowdetail and its comments lose their reference to the action (deletions in a 6b-2 module). The boot readingreportPackagedPermissionSetOverlays(packaged-permission-set-overlay-detection.ts) now names the offline step instead of the action. Comment references in the lock, the projection andwrite-refusals.tsare trimmed.ISecurityService.discardPermissionSetOverlayandPermissionSetOverlayDiscardResultare removed, with their contract test.ERROR_CODE_LEDGER['@objectstack/plugin-security']dropsINVALID_STATEandNOT_FOUND: the removed action was their only producer in that package. Both stay under@objectstack/rest, so the union is unchanged. TheNOT_FOUNDwaiver reason and the ledger test follow.security-permission-set-overlay-discard-retired.api-surface/andexport-origins/are regenerated.packaged-permission-set-lockand its gate.packaged-permission-set-overlay-detectionkeeps its consumer: the boot reading insecurity-plugin.tscalls it, and the discard never imported it.permission-sets.mdx(the "Overlay shadow" remedy),system-context.mdx(row 14 removed; census counts regenerated),metadata-lifecycle.mdxandenvironment-variables.mdx(the permission-set exception), andtenant-audit-census.mdxwith its counts ledger (one write site fewer). Nothing undercontent/docs/releases/.engine-double-contract.pinned.json(regenerated; 2 rows of the deleted test) andobjectql-double-limit.baseline.json(its entry, shrink-only).spec,plugin-securityandrestareminor, BREAKING (the epic's launch-window convention), with a removed → instead table and the ADR-0087registeredmarker.Acceptance notes
packages/spec/src/**(the contract, the error-code ledger and the migration entry), so a same-head contract-tier PASS is owed beforepr_ready.check-empty-changesetrefuses an edit to another PR's note, and correcting a pending release note is the seat's call):.changeset/22307-cold-boot-catalog-refusal.mdtells an operator to use Discard Overlay "on the release you run now", before upgrading. That stays true of the release they run, where the action exists..changeset/15206-reads-environment-only.md:26says a packaged-set fork "keeps its own ruling (detection reading and Discard Overlay) and is still served". After this PR the "Discard Overlay" half is false. The suggested correction is "(the detection reading)"..changeset/22719-rest-write-hook-refusal-sentence.md:16(landed onmainwhile this PR was open) listsPOST /security/permission-sets/:id/discard-overlayamong the routes it repaired. Once this PR lands, that route no longer exists. The suggested correction is to drop it from that row.../objectuiis available here. The removed surface is a server route, a declarative action and a contract type. An objectui file importingPermissionSetOverlayDiscardResultor namingdiscard-overlaywould be the Console Pin Gate's red.system-context.mdxleaves a gap in its numbering (13 → 15). Renumbering would move every later row's key that other text cites.Verification (head
8f992cde3d: the change plus one no-rebase merge oforigin/main490cb6d9fa)Package suites:
plugin-security: test exit 0 (4003 passed, 45 skipped); typecheck exit 0.rest: test exit 0 (5235 passed, 326 skipped); typecheck exit 0.spec: test exit 0 (19220 passed); typecheck exit 0.client: test exit 0 (653 passed); typecheck exit 0.Each was run as
pnpm --filter PKG testandtypecheckbehindos-verify-lock, afterpnpm install --frozen-lockfileand a closure build.Targeted:
standalone-stack-security-catalog-one-holder.test.ts: 6/6.route-ledger-live-mount-parity,permission-set-lock-row-provenanceandpermission-set-clone-boot-unowned-warning: 25/25.runtimeanddogfoodsuites are left to CI. This is a declared narrowing: neither package's source is in the diff, and the one dogfood test file in the diff is deleted.Derived gates:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ranreports 131 derived, 131 run, 0 NOT MEASURED, 0 UNRUN, and every family exits 0. That includescheck:generatedfor spec,check:i18n,check:i18n-stale-fill, the system-context and tenant-audit censuses,check:adr-0087-registration,check:empty-changeset,check:engine-double-contractandcheck:objectql-double-limit.check-route-ledger-censusalso exits 0.Census edits, from the measurement:
execctx-consumer-census.test.ts: 63 → 62 sites, 78 → 77 mentions, 47 → 46 bare.Generated by Claude Code