Repository navigation
feat(plugin-security)!: the security readers read the catalog and the activation ledger (ADR-0131 cutover stage 2a) - #22751
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
…tion ledger (stage 2a, WIP) Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN Co-authored-by: Claude <noreply@anthropic.com>
…per-organization position pins retire Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN Co-authored-by: Claude <noreply@anthropic.com>
…the catalog Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN Co-authored-by: Claude <noreply@anthropic.com>
…h deactivation through the ledger Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN Co-authored-by: Claude <noreply@anthropic.com>
…ermission-sets' into claude/issue-15204-s2a-security-readers
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 35 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 b36770da1cce16fb02e2fc9eb8faa1e68103c6aa && git checkout b36770da1cce16fb02e2fc9eb8faa1e68103c6aa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27 047a8771388ac7e45b7d151d5fb53df959c7bf45 && git checkout -B drift-repro 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27 && git merge --no-ff 047a8771388ac7e45b7d151d5fb53df959c7bf45
node scripts/docs-audit/affected-docs.mjs --json 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27
|
…registered Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN Co-authored-by: Claude <noreply@anthropic.com>
…on-assignment refusal name the ledger and the security catalog Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN Co-authored-by: Claude <noreply@anthropic.com>
…a-security-readers # Conflicts: # packages/core/src/security/resolve-authz-context.ts # packages/core/src/security/security-catalog.ts # packages/plugins/plugin-security/src/__tests__/security-catalog.testkit.ts # packages/plugins/plugin-security/src/explain-engine.test.ts # packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts # packages/plugins/plugin-security/src/security-plugin.ts
Contract reviewServed-tier: Contract-tier review of PR #22751 (stage 2a of the #15204 cutover batch, ① Derived judgments
② Semver level
Clause-②: yes — right. The ③ Boundary flagsDev flags (
Deviations: the core export and comment corrections (answered by the changeset and ①.8); the dogfood fixtures and the baseline ratchet (①.9); the PR base (Q1). Out-of-scope findings, each with a named carrier the seat accepted: Items the landing comment (6104351995) owes this PR's merge round: the docs commit is on this head; the size addendum is the dev's to post (the size is stated above from the PR's own counts). Escalated by this record:
Implemented-by: VERDICT: FAIL One reason, ①.2 / E1: the enforcement-plane fault direction of the plugin's third set source lands unpinned, and the diff's own comment names a pin that does not exist on this head. Every other judgment holds; a PASS record is owed on the head that restores the pin. Generated by Claude Code |
|
os-dev-report addendum (stage 2a, PR #22751): the merge round after stage 1 landed · Head:
Size against the new merge base: 2,699 changed lines, +1,192 / −1,507, 37 files. That is the 35 files measured before, plus the two docs pages below. Generated files: none. This is the size of record. ADR-0087 marker: kept in the gate's own form, Docs edits (commit
Two sentences were false, and both are fixed:
The four pages named for re-reading were checked again against
Gates on
Suites on
The PR stays a draft. Generated by Claude Code |
…closed warn path; the warn names the catalog Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN Co-authored-by: Claude <noreply@anthropic.com>
…ndOne double Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN Co-authored-by: Claude <noreply@anthropic.com>
|
os-dev-report addendum 2 (stage 2a, PR #22751): contract-review patch round 1, for record 6104579158 ①.2 / E1 · Head:
Both commits were reworded before their first push, so the pre-push hook would accept them. No published commit was rewritten, and the pushed tree is byte-identical to the one the runs below measured. E1, the catalog loader's pin, is restored (option 1). It is the new file
The warn now names the catalog. Ablation ( Docs sentence (
Runs. All suites ran under
Size against the merge base: 2,902 changed lines, +1,384 / −1,518, 41 files. That is the 37 files before, plus the new pin, The PR stays a draft. Generated by Claude Code |
…a-security-readers
Contract reviewServed-tier: Contract-tier review of PR #22751 (stage 2a of the #15204 cutover batch, What changed since the FAIL record. A line-by-line comparison of this head's net diff with the one judged on
① Derived judgmentsJudgments 1 and 3 to 11 of 6104579158 carry over unchanged: the delta touches none of their code, and the pages they read are the same on this head except where named below. Restated here only where the delta moves them.
② Semver levelUnchanged from 6104579158, and the changeset file is byte-identical on this head: Clause-②: yes — right, as before: the ③ Boundary flagsThe dev's four Closed by this head: E1 (①.2) and E2 (①.13). Open carriers unchanged: ①.3's residual with seat Q3 (stage 8). The release-cut condition (6102862135, as 6104692060 extends it for objectui's spec pin) is unchanged by this PR: nothing in the diff writes Implemented-by: VERDICT: PASS The one reason of 6104579158 is closed on this head, every other judgment of that record holds, and the four delta items are each judged right above. Generated by Claude Code |
|
os-dev-report addendum 3 (stage 2a, PR #22751): the main merge after 2b · Head: The patch-round-1 content (
Runs on
Size against the merge base: 2,902 changed lines, +1,384 / −1,518, 41 files. Unchanged by this merge. PR state: #22751 reads Generated by Claude Code |
Part of #15204
Clause-②: yes
Stacked on stage 1. The base is
claude/issue-15204-s1-position-permission-sets(PR #22723). GitHub retargets this PR tomainwhen stage 1 merges, because merged head branches are deleted. Stage 1's branch was merged in atd0cab9f8(its patch round 1 and itsmainmerges); the diff below is against that head.Size: 2,694 changed lines (+1,189 / −1,505, 35 files, against
d0cab9f8), under the 3,000 line. Test deletions are included.Stage 2a of the ADR-0131 cutover batch (stage plan 6094501866 row 2, split by claim amendment 6101897723). The plugin-security readers move onto the security catalog and the activation ledger. Stages 2b (plugin-auth, plugin-sharing) and 2c (the activation door for
permissionandposition) are separate PRs. #15204 remains open after this merges.What changes
resolvePermissionSets' row fallback goes (security-plugin.ts).sys_permission_setrow loader (dbLoaderFor, usingresolveOwnOrganizationRow). It is replaced by a catalog loader: the set the security catalog (securityCatalogReaderOf) resolves by name, returned whole.catchin the loader: a catalog read that did not answer throwsAuthzStoreUnavailableError, andPermissionEvaluator.resolvePermissionSetshandles it as it handled a failed row read (fail closed, the [security][observability] resolvePermissionSets swallows dbLoader failures silently — custom permission sets vanish with no log #2565 warn).dropDeactivatedFoldedSets). This replaces the row loader'sisRowActive. The ledger read happens only when a folded name actually resolved, so a request with no collision issues none.permissionSetPageOrRefuse,PermissionSetReadUnansweredError(internal, not exported),callerOrganizationId.The delegated-administration gate (
delegated-admin-gate.ts, ADR-0090 D12). This answers stage 1's Q1.setsBoundToPositionreads the position's definitionpermissionSetsand each set'sadminScopefrom the catalog. The junction rows are not read. So the D12 containment check and the assignable-positions listing judge what an assignment actually grants, a binding declared only on the definition included.[], which approved the allowlist check.positionIsDelegatablereads the definition'sdelegatable.describeDelegableScopelists the catalog's positions (anchors excluded), so a registry-only position is offered.resolveOwnPositionis deleted: the catalog is one per deployment (feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 Q4 = A).readGrantSetRows. A re-pointed grant reads itspermission_set_idto a name (the grant id lookup), and then the catalog body.sys_position_permission_setandsys_permission_settables themselves (assertBindingWrite,assertSetAuthoring,positionById,loadSetRowById). They govern writes to tables that still exist; they are not readers of authority.The explainer (
explain-engine.ts).readDisabledCatalogNames, the resolver's own ledger read, so explain and enforce agree.readGrantSetRowsandisRowActiveare no longer imported.readGrantSetRows(grant-permission-set-name.ts) has no reader left and is deleted. What remains in the file is the name-stamping hook pair, which derivespermission_setfrompermission_set_id, plusgrantSetNameOf. That half goes with the id column (stage 8 or C7b).The position-assignment refusal (
position-catalog-refusal.ts).sys_user_position.positionvalue must name a position the security catalog holds. Before, it had to name asys_positionrow.Clause-②: yes. Pinned bybuiltin-positions.boot(a stack-declared position with no row) andposition-catalog-refusal.sys_user_positionrow whosepositionnames nosys_positioncatalog row — 201 with nothing resolvable (RE-CUT: the originally reported resolution-path defect is disproved, see the 2026-09-09 measurement) #16712's predicate).sys_positionrow id, under the writer's context, and names it only when the catalog resolves that name.Q (a): one holder of a position name per deployment (
position-write-through.ts). Measured on this base:single, a Setup create, or a rename into a name, that an environment definition already held (another organization's Setup row, a metadata-door save, an app stack's declaration) went through. The write-through then overwrote that definition with the new row's label and description and an emptypermissionSets.409 UNIQUE_VIOLATION, the wire answer a second row of the name in one organization already gets, and the row write is undone. The error is a new in-packagePositionNameConflictError, which stamps the same registered constants asPermissionSetNameConflictError.@objectstack/core.readDisabledCatalogNamesis exported, so the explainer and the plugin share the resolver's one ledger read.Equivalence
grant-readers-by-name.golden.test.ts: the recorded golden is unchanged in all three postures. Its member now holds a set declared in the catalog and switched off in the ledger, instead of a row withactive: false.explain-enforce-parity: green, 211 tests together withexplain-engine.adminScopecontainment, anchors, holdings per organization) are green on catalog-bound harnesses.resolve-authz-context.batch-equivalence.golden.jsonis untouched: core's resolver is unchanged apart from the export.Tests
The fake engines and harnesses that bound no catalog now declare one. Most use the plugin-security testkit (
bindCatalogFromTables), which now also carriesadminScopeanddelegatable; the rest register items on the real registry. Deactivation fixtures use the ledger.Deleted, because their premise retired with the per-organization catalog:
delegated-admin-gate-position-organization.test.ts: positions resolved per organization row. Its rule is replaced bydelegated-admin-gate-catalog.test.tsand a rewritten block indelegated-admin-gate.test.ts.engine-find-bare-array.pin.test.ts.resolve-permission-sets-for-context.pin.test.ts.Rewritten:
permission-set-active.test.ts(the ledger is the switch, and the row is not read). REVERSED pin inorgless-position-name-fold.test.ts: an organization's row-only set resolves for nobody, its own active members included (#15196 Q3 = A).Two dogfood fixtures authored their position or set as a row, and this PR is what stops rows from counting. They now declare it through the metadata door (
PUT /meta/position/NAME,PUT /meta/permission/NAME):delegation-of-duty: the gate read the row'sdelegatable.showcase-scope-depth-fallback: the fallback set is requested by name, so the row loader used to serve it.The
/me/appsand FLS dogfood probes failed for the same reason on stage 1's own head; stage 1's patch round 1 moved them, and they are not touched here.scripts/objectql-double-limit.baseline.jsonloses thepermission-set-active.test.tsentry: the rewritten file's doubles are clean (ratchet down).Ablations (
scripts/ablation-replace.mjs, each restored byte-identical withgit diff HEADempty):dropDeactivatedFoldedSetscall:permission-set-active2 failed / 5 passed.setsBoundToPositionignorepermissionSets:delegated-admin-gate-catalog3 failed / 5 passed.position-write-through3 failed / 48 passed.Acceptance notes
permissionorpositionuntil stage 2c.auto-org-admin-grant.tsreadssys_permission_setonly to find the row id a grant'spermission_set_idforeign key must name (required: trueon that column), and the ids a revoke must reach. That is the grant id lookup that order correction 4 assigns to stage 8 or C7b, not an authority read. It is left as is.security-plugin.tsstill importsresolveOwnOrganizationRowandseedCtx, but only forbindBaselineToEveryone(retired by 6a, "only delete").PermissionEvaluator.resolvePermissionSetsreads the metadata list first and the catalog loader third. A name that both the metadata service and an ADR-0005 registry overlay answer takes the metadata body, while the resolver's catalog read takes the overlay. No such pair was measured in the repository's fixtures. This is recorded for stage 8, which retires the bootstrap list.GRANTS_CACHE_WATCHED_OBJECTSstill watches the three retired tables (stage 1's note; harmless over-invalidation).Verification
All runs are local. Heavy runs ran under
os-verify-lock, and each exit code was captured before any pipe.On the final head
6d6c6543(stage 1 atd0cab9f8):typecheckgreen (test layer included).On
4c9f967be(stage 1 atbf6bdf07; the merge since brought only stage 1'smainmerges):typecheckgreen.Before the first stage 1 merge (
5a960f8ae, stage 1 at83b54d84), these suites were green: plugin-auth, rest, runtime, organizations, service-automation, verify, cli (unit), plugin-approvals, hono, example-crm, example-showcase and cloud-connection. The core golden (resolve-authz-context.batch-equivalence+security-catalog) was also green, 50 passed.Gates, on
6d6c6543:dispatch-gates --commandsderived 134 families, and--ranwith recorded exit codes reports 134 run, 0 NOT-MEASURED. One family exits 1:check:platform-checklist, with 4 ABSENT SYMBOL anchors inmetadata-protocol/src/protocol.tsandservice-storage/src/attachment-access-hooks.ts. Stage 1 measured the identical findings onmain, and this diff touches neither file.Fixed while sweeping, each re-run green:
check:undeclared-dep-imports: an@objectstack/objectqlimport, a devDependency here, replaced by the in-package error.check:objectql-double-limitandcheck:where-matcher: the test doubles.The teardown-shape self-test needed its pinned fixture commit, fetched by sha (
git fetch origin 621a4876…).check:skill-examplesandcheck:dual-build-cjs-loadsneeded their packages built first. All three were re-run green.The repo-wide
pnpm lintwas not run locally.Generated by Claude Code