Repository navigation
feat(runtime): activation door for positions and permission sets on sys_metadata_activation (stage 2c) - #22736
Conversation
…d bodies from the catalog Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…eline Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…ecorded Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…declared at kernel:ready Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…ledger regenerated Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…ssionSets through the write-through Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…are the catalog Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…-position-permission-sets
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…c entry Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U Co-authored-by: Claude <noreply@anthropic.com>
…activation ledger The resolver honours ADR-0049 deactivation through sys_metadata_activation (types position and permission) and no longer reads the catalog row's active column. convertDeactivatedCatalogRows turns existing active=false rows into ledger rows for the upgrade ceremony; it is not wired into boot. A composition whose registry has no ledger object issues no ledger read. The admin-standing surface now derives from the ledger; the last-admin guard refuses a ledger write that switches admin_full_access off. The name-fold warning names permissionSets as the governed remedy. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
The resolver reads deactivation from sys_metadata_activation; the install fixtures' grant maps answer it empty, as they answer the other authz tables. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…-position-permission-sets # Conflicts: # packages/plugins/plugin-security/src/security-plugin.ts
The merged capability declarations are told apart by _packageId; the test registry now stamps it as the engine registry does. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…Sets; the ledger-guard test double holds the caller's bound Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
… zh-CN, ja-JP and es-ES Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…-position-permission-sets
…adata_activation Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
…solver effect Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
… gate; engine-double ledger learns both doubles Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
…ermission-sets' into claude/issue-15204-s2c-catalog-activation-door
Seat note:
|
…c-catalog-activation-door # Conflicts: # packages/spec/src/migrations/registry.ts
…tch is described Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
…'s active flag decides no grant, and names who still reads it Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
…atalog's activation switch Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
…c-catalog-activation-door
… pair; row-active.ts names its remaining readers Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Stage 2c of #15204 (amendment 6101897723; seat answers 6103293232; pin rule 6103398921; stage 1 ACCEPT 6102862135 and Landed 6104351995). Read on GitHub only: the card body and all 56 comments, the PR body, its 17-file list, the net diff against ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…dience anchors; their sealed refusal names no switch Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
…c-catalog-activation-door
…'s refusal keeps the managed seal's edit remedy; the switch is documented beside the table Claude-Session: https://claude.ai/code/session_015LutkhFn8wzgD5J86PtpYN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Supersedes 6104861792 (PASS on ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #15204
Clause-②: yes
This is stage 2c only: the activation door for
permissionandposition. Stages 2a, 2b and stage 1 have landed; the rest of the card stays open, and #15204 remains open after this merges. The PR was opened stacked on stage 1 and was retargeted tomainwhen stage 1 merged.Size: 1,352 changed lines against the merge base
149294c02c(+1,279 / −73, 17 files), under the 3,000 limit.mainwas merged in with no rebase (stage 1, then 2b and stage 3).The route (for objectui C9)
POST /api/v1/security/_activation/:type/:name, with:typeeitherpositionorpermission. The body is{ enabled?: boolean }andenableddefaults totrue. With project scoping on, the route is also mounted under/api/v1/environments/:environmentId/security/_activation/:type/:name.200,data={ type, name, enabled }.401for anonymous callers (the/securitydomain floor).403 PERMISSION_DENIEDwithoutmanage_metadata, or for a tenant admin under agrouporisolatedposture.400for a wrong path shape or a type other than the two.400 VALIDATION_FAILEDfor a bad body.404for a name that does not resolve in the security catalog.503 SERVICE_UNAVAILABLEwhen no catalog is bound or the catalog cannot be read.501when the composition carries no activation ledger.403 PERMISSION_DENIEDfrom the last-admin guard when the request switchesadmin_full_accessoff.Setup's Deactivate on the position and permission-set pages should call this route. That wiring is C9's follow-up, not part of this PR.
What changed
Stage 1 makes
resolveUserAuthzGrantsread deactivation forpositionandpermissionfromsys_metadata_activation, and never from the catalog row'sactive. Measured on stage 1's head83b54d84: no door wrote that ledger for these two types. Only the flow store (toggleFlow) and the action store (setActionActive) did, andsys_metadata_activationdeclaresapiMethods: ['get', 'list'], so the data API cannot write it. Setup's Deactivate therefore switched nothing off. This PR adds the write half. The release-cut condition (a) on the card depends on it.packages/runtime/src/domains/catalog-activation.ts(new): the door. The steps run in this order:refuseUngrantedActivationAuthoring(manage_metadata), thenrefuseUngrantedActivationWrite(the ADR-0126 §5 posture rule). They are the same functions the action door calls, with two newActivationSubjects.securityCatalogReaderOf(engine), the reader stage 1 binds and the resolver reads, so the door and the resolver agree on which names exist.ObjectStoreMetadataActivationStore(engine, type), the row contract the flow and action stores already share, carrying the definition's_packageId.active, and authors no clone. The write goes through the engine, so the ledger's hooks run. Stage 1'srefuseLedgerSwitchingAdminOffrefuses switchingadmin_full_accessoff (stage 2b replaces that hook), and the grants cache drops what it holds for the ledger.domains/activation-gate.ts: addsPOSITION_ACTIVATION_SUBJECTandPERMISSION_SET_ACTIVATION_SUBJECT.readActivationBodymoves here from the action door unchanged, so both_activationdoors read one body contract. The action door's messages and envelope are byte-identical, and its suites pass unchanged.domains/actions.ts: callsreadActivationBodyinstead of its inline copy.domains/security.ts: the_activationarm sits after the anonymous floor and before thesecurityservice probe. The door does not use that service.dispatcher-plugin.ts:registerSecurityActivationRoutesmounts the one route at the same call sites as the action routes. The rest of/securityis the REST server's on the wire. The_activationliteral collides with none of its routes, because machine names cannot begin with_.route-ledger.ts: one row with dispositionserver-only, for the action door's reason: its caller is the Setup console.scripts/check-route-envelope.mjsdeclarescatalog-activation.tsinDISPATCHER_DOMAINSas{ handBuilt: 0 }, because every answer goes throughdeps.*.--fix).scripts/engine-double-contract.pinned.jsonrecords the two new engine doubles. Theirupdateroutes throughassertEngineUpdateDispatch..changeset/15204-s2c-catalog-activation-door.md:@objectstack/runtimeminor,Clause-②: yes, with the route named.Where the producer lives. It is
packages/runtime/src/domains/, as the dispatch expected. The gate, the two sibling doors and the dispatcher mount are all there. The/securityREST surface in@objectstack/restwas not used. Putting the door there would have split the activation authority across two packages.Who may call it: the same authority as the flow and action doors (#22621 → A). Nothing is widened.
Tests
domains/catalog-activation-door.test.ts(37 cases):single, no tenancy service, nomanage_metadata, anonymous, andgroup/isolatedfor the tenant admin and for the operator. Every refusal asserts that nothing was written.enabled, the empty-body default, unknown names, a name of the other type, no catalog bound, no ledger object, the package carried onto the row, and re-enable updating the single row.resolveUserAuthzGrantsover the same engine stops granting through a switched-off position (the holder's direct set is untouched) and grants again when it is switched back on. A switched-off set stops granting both directly and through a position. Each leg has a control.domains/tenancy-posture-outage-gates.test.ts: the third activation door joins the shared outage pin: a throwingtenancygives 503 with nothing written; never registered admits the org admin; scoped and healthy refuses the tenant admin 403 and admits the operator 200.qa/dogfood/test/catalog-activation-door.dogfood.test.ts(6 cases, booted showcase):exec→showcase_executivefor a signed-up holder, then grants it again. A control precedes the flip.admin_full_accessoff with 403PERMISSION_DENIED, and no row lands.active: enabledchanged toactive: true(viascripts/ablation-replace.mjs), 4 of 37 cases go red: both "flips it" row cases and both resolver-effect cases. Restored byte-identical: blobc5752231equals HEAD, andgit diff HEADis empty.Local verification
Exit codes were captured before any pipe. Heavy runs went through
os-verify-lock.@objectstack/runtime:typecheckexit 0, which includescheck:test-typecheckfor the test files.test: 349 files, 4,975 passed, 19 skipped.@objectstack/client(readsROUTE_LEDGERas source): 51 files, 653 passed.@objectstack/dogfood:typecheckexit 0. The new file passed 6 of 6,route-ledger-live-mount-parity8 of 8,authz-conformance55 of 55, andshowcase-anonymous-deny-surfaces+permission-set-discard-overlay-eligibility+explain-write-door-parity106 of 106.ROUTE_LEDGERreaders:metadata-protocolprotocol.destructive-409-face-inventory26/26,plugin-sharingshare-link-envelope.conformance26/26,restdirect-mount-introspection13/13.check:generatedreports all 15 artifacts up to date.dispatch-gates --commandsderives 147 at1b58797a.--ranreports 147 derived, 147 run, 0 NOT-MEASURED, 0 UNRUN, with every exit code recorded. 146 exit 0. One exits 1:check:platform-checklist, with seven ABSENT SYMBOL findings inmetadata-protocol/src/protocol.tsandservice-storage/src/attachment-access-hooks.ts. That is the same red stage 1 recorded as identical onmain; neither file is in this diff.pnpm lint, which CI runs.Acceptance notes
metadata-protocol'sPACKAGED_BASE_REGIME.permissionstill names only the clone in the packaged-base refusal sentence. It could also name this switch. The sentence stays true as it is. Left for a carrier that touches that file.GRANTS_CACHE_WATCHED_OBJECTSalready watches the ledger (stage 1), so this door's writes invalidate cached grants with no change here.capabilityhas no switch: it is code-only on the regime, and the door refuses it with 400.sdk.Generated by Claude Code
Landing update by the epic seat (
session_01Rerax7QTjKMPCUZxQUtPFR): first lines and size updated to the landing heada27c4db8. Since the body was written, the door also refuses the audience anchorsposition/everyoneandposition/guest(400, no write), and theadmin_full_accessdogfood case pins the ruled pair after 2b. Contract record 6105494730 (PASS).