From 07e8615e5ecc12403f5239d48a7a80d305bbe0b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 03:02:03 +0000 Subject: [PATCH 1/2] chore(spec): adopt the adjudicated proof->ledger bindings, and correct the stale systemPermissions note Executes the spec seat's adjudication on PR #10934 (issuecomment-5376518715). Each binding was re-verified against merged origin/main @ 2b5a7ce, not the 23e27af370 the adjudication was written against. - showcase-fls-read-mask-strip -> permission.fields.readable (not editable). - showcase-crud-persona-matrix -> all four permission.objects.allow* verbs. - sharing-rule-org-less-caller -> permission.systemPermissions, whose ledger note turned out STALE on re-verification, so it is corrected here and the binding adopted in the same PR, per the ruling's own conditional. BOUND_PROOF_PATHS moves 20 -> 26: six additions, no removals. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP --- packages/spec/liveness/permission.json | 26 +++++-- .../spec/scripts/liveness/proof-registry.mts | 73 ++++++++++++------- .../scripts/liveness/proof-registry.test.ts | 23 ++++++ 3 files changed, 88 insertions(+), 34 deletions(-) diff --git a/packages/spec/liveness/permission.json b/packages/spec/liveness/permission.json index c7b40c47b8..4f6106b95f 100644 --- a/packages/spec/liveness/permission.json +++ b/packages/spec/liveness/permission.json @@ -49,22 +49,30 @@ "allowCreate": { "status": "live", "verifiedAt": "2026-07-30", - "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:15 (OPERATION_TO_PERMISSION insert→allowCreate)" + "proof": "packages/qa/dogfood/test/showcase-crud-persona-matrix.dogfood.test.ts#showcase-crud-persona-matrix", + "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:15 (OPERATION_TO_PERMISSION insert→allowCreate)", + "note": "Proof-bound 2026-08-23 (#10959, adjudicated on PR #10934): the persona × CRUD matrix runs the create verb over real HTTP for every showcase permission set, in BOTH directions per cell — an allowed create returns an id and the row is really there, a denied one is 403 PERMISSION_DENIED and persisted nothing. Cells are judged as a UNION with the everyone-baseline set (ADR-0090 D5). One proof binds all four allow* verbs; multi-entry binding has precedent in `semantic-roles`." }, "allowRead": { "status": "live", "verifiedAt": "2026-07-30", - "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:15 (OPERATION_TO_PERMISSION find/findOne/count/aggregate→allowRead)" + "proof": "packages/qa/dogfood/test/showcase-crud-persona-matrix.dogfood.test.ts#showcase-crud-persona-matrix", + "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:15 (OPERATION_TO_PERMISSION find/findOne/count/aggregate→allowRead)", + "note": "Proof-bound 2026-08-23 (#10959) — the read verb of the same persona × CRUD matrix, both directions per cell (a readable list answers 200 with rows; a denied one is 403 PERMISSION_DENIED). See `allowCreate` for the binding rationale." }, "allowEdit": { "status": "live", "verifiedAt": "2026-07-30", - "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:15 (OPERATION_TO_PERMISSION update→allowEdit)" + "proof": "packages/qa/dogfood/test/showcase-crud-persona-matrix.dogfood.test.ts#showcase-crud-persona-matrix", + "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:15 (OPERATION_TO_PERMISSION update→allowEdit)", + "note": "Proof-bound 2026-08-23 (#10959) — the edit verb of the same persona × CRUD matrix, both directions per cell, asserted on POST-STATE (the allowed edit persisted; the denied edit changed nothing). See `allowCreate` for the binding rationale." }, "allowDelete": { "status": "live", "verifiedAt": "2026-07-30", - "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:15 (OPERATION_TO_PERMISSION delete→allowDelete)" + "proof": "packages/qa/dogfood/test/showcase-crud-persona-matrix.dogfood.test.ts#showcase-crud-persona-matrix", + "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:15 (OPERATION_TO_PERMISSION delete→allowDelete)", + "note": "Proof-bound 2026-08-23 (#10959) — the delete verb of the same persona × CRUD matrix, both directions per cell, asserted on POST-STATE (the deleted row is gone; the denied delete left the row standing). See `allowCreate` for the binding rationale." }, "allowExport": { "status": "live", @@ -122,8 +130,9 @@ "readable": { "status": "live", "verifiedAt": "2026-07-30", + "proof": "packages/qa/dogfood/test/showcase-fls-read-mask-strip.dogfood.test.ts#showcase-fls-read-mask-strip", "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:301", - "note": "FLS read-mask." + "note": "FLS read-mask. Proof-bound 2026-08-23 (#10959, adjudicated on PR #10934): the proof AUTHORS a scratch permission set carrying `readable: false` and asserts the runtime outcome both ways on the same field, row and request — the key is ABSENT from the wire (`'budget' in record` is false, which `toBeUndefined()` cannot distinguish from a mask), while the entitled caller gets the real value. The sibling `editable` is deliberately NOT bound to this proof: that file authors the key but asserts its refusal as a consequence of unreadability, not as the write-deny axis (`showcase-permission-zoo` pins that half)." }, "editable": { "status": "live", @@ -135,9 +144,10 @@ }, "systemPermissions": { "status": "live", - "verifiedAt": "2026-07-30", - "evidence": "packages/plugins/plugin-hono-server/src/hono-plugin.ts:1222", - "note": "PARTIAL — app-entry/nav visibility only, not a general capability gate." + "verifiedAt": "2026-08-23", + "proof": "packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts#sharing-rule-org-less-caller", + "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:267 (getSystemPermissions — ADR-0066 D3 union of the capabilities a caller's permission sets grant, into ExecutionContext.systemPermissions) + packages/plugins/plugin-sharing/src/sharing-rule-service.ts:136 (assertCanManageRules — ADR-0111 D6: every sharing-rule verb, list and get included, refuses PERMISSION_DENIED without `manage_sharing`, enforced in the SERVICE so non-REST callers are covered) + packages/plugins/plugin-sharing/src/sharing-plugin.ts:993 + packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:897 (/auth/me/apps — `AppSchema.requiredPermissions ⊆ ctx.systemPermissions`, the app-entry/nav half)", + "note": "Re-verified 2026-08-23 (#10959) and the note CORRECTED — the previous text (\"PARTIAL — app-entry/nav visibility only, not a general capability gate\") was stale in both halves. (a) Its evidence pointer `plugin-hono-server/src/hono-plugin.ts:1222` no longer exists: that file is 717 lines and contains no `systemPermissions` reference at all — the app-entry consumer moved to `current-user-endpoints.ts`. (b) The scoping claim is falsified by ADR-0111 D6 (Accepted 2026-07-30, P0 implemented): `SharingRuleService.assertCanManageRules` reads `context.systemPermissions` and refuses the whole sharing-rule surface without `manage_sharing`, which is a DATA-LAYER authorization gate, not nav visibility. The bound proof measures exactly that: it authors `system_permissions: ['manage_sharing']` on a permission set and asserts the refusal it gets back is the ORG-scope refusal and explicitly NOT `/requires the manage_sharing capability/` — i.e. the authored capability really did clear the service gate — with the org-bound holder of the same grant reading its own tenant (200) as the entitled contrast, over two organizations so a single-tenant fixture cannot pass on the broken build." }, "tabPermissions": { "status": "live", diff --git a/packages/spec/scripts/liveness/proof-registry.mts b/packages/spec/scripts/liveness/proof-registry.mts index 078645d146..e9fb7bf45f 100644 --- a/packages/spec/scripts/liveness/proof-registry.mts +++ b/packages/spec/scripts/liveness/proof-registry.mts @@ -708,15 +708,25 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ proofId: 'sharing-rule-org-less-caller', proofRef: 'packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts#sharing-rule-org-less-caller', - bound: false, - ledgerBindings: [], - blockedReason: - 'binding PROPOSED, not adopted: the file authors `system_permissions: [\'manage_sharing\']` on a ' - + 'permission set, so `permission.systemPermissions` (live, carrying no proof) is a candidate — ' - + "but that entry's note scopes it to \"app-entry/nav visibility only, not a general capability " - + 'gate\", which this proof measures as narrower than reality. Re-verifying the note is part of ' - + 'the binding decision, and a ledger edit is the spec seat\'s review (#10773), not a ' - + 'registration side effect.', + bound: true, + // Bound 2026-08-23 (#10959) on the spec seat's adjudication of PR #10934, + // whose item ③ was conditional: bind only if the entry's note turned out + // stale. The re-verification says stale on BOTH halves, so the note was + // corrected in the same PR and the binding follows. + // • The old evidence pointer (`plugin-hono-server/src/hono-plugin.ts:1222`) + // does not exist — that file is 717 lines and never mentions + // `systemPermissions`; the app-entry consumer moved to + // `current-user-endpoints.ts` (`/auth/me/apps`). + // • The old note's scoping claim ("app-entry/nav visibility only, not a + // general capability gate") is falsified by ADR-0111 D6: + // `SharingRuleService.assertCanManageRules` reads + // `context.systemPermissions` and refuses every sharing-rule verb + // without `manage_sharing` — a data-layer gate, not nav visibility. + // The proof authors the key and proves the gate was CLEARED by it: the + // refusal it asserts is the org-scope one and explicitly NOT + // /requires the manage_sharing capability/, with the org-bound holder of + // the same grant reading its own tenant as the entitled contrast. + ledgerBindings: [{ type: 'permission', path: 'systemPermissions' }], }, { id: 'crud-persona-matrix', @@ -733,14 +743,22 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ proofId: 'showcase-crud-persona-matrix', proofRef: 'packages/qa/dogfood/test/showcase-crud-persona-matrix.dogfood.test.ts#showcase-crud-persona-matrix', - bound: false, - ledgerBindings: [], - blockedReason: - 'binding PROPOSED, not adopted: the four cells map exactly onto `permission.objects.allowCreate ' - + '/ allowRead / allowEdit / allowDelete` (all live, none carrying a proof), and a multi-entry ' - + 'binding has precedent in `semantic-roles`. Whether a persona-breadth matrix should anchor ' - + 'four entries or a chosen subset is a judgment call, and the ledger edit is the spec seat\'s ' - + 'review (#10773).', + bound: true, + // Bound 2026-08-23 (#10959) — ADOPT ALL FOUR, on the spec seat's + // adjudication of PR #10934. The scope worry the registration raised (one + // breadth proof anchoring four properties) is answered by the file's own + // shape: the EXACT allow/deny split is asserted (54 allow / 54 deny, one + // verdict per set × object × verb), so a narrowing sweep — or a persona + // silently failing to provision — breaks the build instead of quietly + // shrinking what these four entries cite. Each verb is exercised in both + // directions per cell and on post-state, not just on status codes. + // Multi-entry binding has precedent in `semantic-roles`, which binds three. + ledgerBindings: [ + { type: 'permission', path: 'objects.allowCreate' }, + { type: 'permission', path: 'objects.allowRead' }, + { type: 'permission', path: 'objects.allowEdit' }, + { type: 'permission', path: 'objects.allowDelete' }, + ], }, { id: 'fls-read-strip', @@ -756,15 +774,18 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ proofId: 'showcase-fls-read-mask-strip', proofRef: 'packages/qa/dogfood/test/showcase-fls-read-mask-strip.dogfood.test.ts#showcase-fls-read-mask-strip', - bound: false, - ledgerBindings: [], - blockedReason: - 'binding PROPOSED, not adopted — the strongest of the three: the file AUTHORS a scratch ' - + 'permission set carrying `readable: false` and asserts the runtime outcome both ways, and ' - + '`permission.fields.readable` (live, note "FLS read-mask") carries no proof. Not ' - + '`fields.editable` alongside it: the file authors that key but asserts its refusal as a ' - + 'consequence of unreadability rather than as the write-deny axis, which ' - + '`showcase-permission-zoo` already pins. The ledger edit is the spec seat\'s review (#10773).', + bound: true, + // Bound 2026-08-23 (#10959) — the strongest of the three, on the spec + // seat's adjudication of PR #10934. The file AUTHORS a scratch permission + // set carrying `readable: false` and asserts the runtime outcome both ways + // on the SAME field, row and request, so `permission.fields.readable`'s + // `live` status is exactly what it gates. + // NOT `fields.editable` alongside it: the file authors that key but + // asserts its refusal as a CONSEQUENCE of unreadability rather than as the + // write-deny axis, which `showcase-permission-zoo` already pins. Binding it + // would repeat the owner-anchor / `allowTransfer` mistake — a proof cited + // for a property it does not exercise. + ledgerBindings: [{ type: 'permission', path: 'fields.readable' }], }, ]; diff --git a/packages/spec/scripts/liveness/proof-registry.test.ts b/packages/spec/scripts/liveness/proof-registry.test.ts index 82ef49e716..2b62463070 100644 --- a/packages/spec/scripts/liveness/proof-registry.test.ts +++ b/packages/spec/scripts/liveness/proof-registry.test.ts @@ -136,6 +136,29 @@ describe('registry invariants', () => { // without a runtime proof; its new dogfood file asserts post-state in // three postures (widen / independent-of-read / fail-closed seam). 'permission/objects.writeScope', + // Bound 2026-08-23 (#10959) — the three proofs PR #10934 registered + // with a PROPOSED binding, adopted after the spec seat adjudicated + // them. Six paths, because two of the three are multi-entry. + // + // `showcase-fls-read-mask-strip` → the READ axis only. Its sibling + // `permission/fields.editable` is deliberately absent: that proof + // authors the key but asserts its refusal as a consequence of + // unreadability, not as the write-deny axis. + 'permission/fields.readable', + // `showcase-crud-persona-matrix` → all four verbs. The exact allow/deny + // split is asserted per cell, so a narrowing sweep breaks the build + // rather than quietly shrinking what these four cite (`semantic-roles` + // is the multi-entry precedent). + 'permission/objects.allowCreate', + 'permission/objects.allowRead', + 'permission/objects.allowEdit', + 'permission/objects.allowDelete', + // `sharing-rule-org-less-caller` → `systemPermissions`. The card held + // this one back pending a measurement: its ledger note claimed + // "app-entry/nav visibility only". The note was stale — ADR-0111 D6 + // makes it a data-layer gate at `SharingRuleService` — so the note was + // corrected and the binding adopted in the same PR. + 'permission/systemPermissions', ].sort(), ); }); From 6dba71bcec3e7bf4acbf176e95d4f8bb5c74be86 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 03:41:57 +0000 Subject: [PATCH 2/2] fix(spec): name the manage_platform_settings admit path in the systemPermissions citation PM review of PR #11209: the new evidence/note text said the sharing-rule surface "refuses without manage_sharing", but assertCanManageRules (sharing-rule-service.ts:139) admits `manage_sharing` OR the legacy `manage_platform_settings` admin override, and bypasses for system contexts. Over-claiming an admit set is the declared-not-enforced class this card polices, so the three sites carrying the claim now name it. The classification is unchanged: every member of the admit set is read from `systemPermissions`, which is what the bound entry classifies. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP --- packages/spec/liveness/permission.json | 4 ++-- packages/spec/scripts/liveness/proof-registry.mts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/spec/liveness/permission.json b/packages/spec/liveness/permission.json index 4f6106b95f..edf8782b53 100644 --- a/packages/spec/liveness/permission.json +++ b/packages/spec/liveness/permission.json @@ -146,8 +146,8 @@ "status": "live", "verifiedAt": "2026-08-23", "proof": "packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts#sharing-rule-org-less-caller", - "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:267 (getSystemPermissions — ADR-0066 D3 union of the capabilities a caller's permission sets grant, into ExecutionContext.systemPermissions) + packages/plugins/plugin-sharing/src/sharing-rule-service.ts:136 (assertCanManageRules — ADR-0111 D6: every sharing-rule verb, list and get included, refuses PERMISSION_DENIED without `manage_sharing`, enforced in the SERVICE so non-REST callers are covered) + packages/plugins/plugin-sharing/src/sharing-plugin.ts:993 + packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:897 (/auth/me/apps — `AppSchema.requiredPermissions ⊆ ctx.systemPermissions`, the app-entry/nav half)", - "note": "Re-verified 2026-08-23 (#10959) and the note CORRECTED — the previous text (\"PARTIAL — app-entry/nav visibility only, not a general capability gate\") was stale in both halves. (a) Its evidence pointer `plugin-hono-server/src/hono-plugin.ts:1222` no longer exists: that file is 717 lines and contains no `systemPermissions` reference at all — the app-entry consumer moved to `current-user-endpoints.ts`. (b) The scoping claim is falsified by ADR-0111 D6 (Accepted 2026-07-30, P0 implemented): `SharingRuleService.assertCanManageRules` reads `context.systemPermissions` and refuses the whole sharing-rule surface without `manage_sharing`, which is a DATA-LAYER authorization gate, not nav visibility. The bound proof measures exactly that: it authors `system_permissions: ['manage_sharing']` on a permission set and asserts the refusal it gets back is the ORG-scope refusal and explicitly NOT `/requires the manage_sharing capability/` — i.e. the authored capability really did clear the service gate — with the org-bound holder of the same grant reading its own tenant (200) as the entitled contrast, over two organizations so a single-tenant fixture cannot pass on the broken build." + "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:267 (getSystemPermissions — ADR-0066 D3 union of the capabilities a caller's permission sets grant, into ExecutionContext.systemPermissions) + packages/plugins/plugin-sharing/src/sharing-rule-service.ts:136 (assertCanManageRules — ADR-0111 D6: every sharing-rule verb, list and get included, refuses PERMISSION_DENIED unless the caller holds `manage_sharing` OR the legacy `manage_platform_settings` admin override, with system contexts bypassing; enforced in the SERVICE so non-REST callers are covered) + packages/plugins/plugin-sharing/src/sharing-plugin.ts:993 + packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:897 (/auth/me/apps — `AppSchema.requiredPermissions ⊆ ctx.systemPermissions`, the app-entry/nav half)", + "note": "Re-verified 2026-08-23 (#10959) and the note CORRECTED — the previous text (\"PARTIAL — app-entry/nav visibility only, not a general capability gate\") was stale in both halves. (a) Its evidence pointer `plugin-hono-server/src/hono-plugin.ts:1222` no longer exists: that file is 717 lines and contains no `systemPermissions` reference at all — the app-entry consumer moved to `current-user-endpoints.ts`. (b) The scoping claim is falsified by ADR-0111 D6 (Accepted 2026-07-30, P0 implemented): `SharingRuleService.assertCanManageRules` reads `context.systemPermissions` and refuses the whole sharing-rule surface unless the caller holds `manage_sharing` or the legacy `manage_platform_settings` admin override (system contexts bypass), which is a DATA-LAYER authorization gate, not nav visibility — the admit set is wider than one capability, but every member of it is read from `systemPermissions`, which is what this entry classifies. The bound proof measures exactly that: it authors `system_permissions: ['manage_sharing']` on a permission set and asserts the refusal it gets back is the ORG-scope refusal and explicitly NOT `/requires the manage_sharing capability/` — i.e. the authored capability really did clear the service gate — with the org-bound holder of the same grant reading its own tenant (200) as the entitled contrast, over two organizations so a single-tenant fixture cannot pass on the broken build." }, "tabPermissions": { "status": "live", diff --git a/packages/spec/scripts/liveness/proof-registry.mts b/packages/spec/scripts/liveness/proof-registry.mts index e9fb7bf45f..11f63680f9 100644 --- a/packages/spec/scripts/liveness/proof-registry.mts +++ b/packages/spec/scripts/liveness/proof-registry.mts @@ -721,7 +721,11 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ // general capability gate") is falsified by ADR-0111 D6: // `SharingRuleService.assertCanManageRules` reads // `context.systemPermissions` and refuses every sharing-rule verb - // without `manage_sharing` — a data-layer gate, not nav visibility. + // unless the caller holds `manage_sharing` or the legacy + // `manage_platform_settings` admin override (system contexts bypass) + // — a data-layer gate, not nav visibility. The admit set is wider than + // one capability, but every member of it is read from + // `systemPermissions`, which is what the bound entry classifies. // The proof authors the key and proves the gate was CLEARED by it: the // refusal it asserts is the org-scope one and explicitly NOT // /requires the manage_sharing capability/, with the org-bound holder of