From 56b4bebe0fadefcaf340c60b906f1da2218a85eb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 13:00:44 +0000 Subject: [PATCH] =?UTF-8?q?feat(spec):=20register=20the=2013=20orphan=20do?= =?UTF-8?q?gfood=20proofs=20=E2=80=94=20five=20advance=20the=20ratchet,=20?= =?UTF-8?q?eight=20say=20why=20they=20can't?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate flagged 13 `@proof:` tags no class in proof-registry.mts claimed. Silencing that is trivial and worthless; the useful question is the one ADR-0054 §3 asks: is there an authorable property whose `live` status this proof gates? FIVE HAVE ONE — bound, taking the ratchet from 10 bound paths to 17: attachments-permission-matrix -> object.enable.files the #2727 opt-in gate in BOTH directions: the fixture carries a deliberate non-declaring object (att_nofiles) that must be refused 403 FILES_DISABLED showcase-d3-d4-capabilities -> permission.rowLevelSecurity.check authors check:'owner == current_user.email', proves POST-image validation (distinct from `using`, which filters the pre-image) showcase-scope-depth -> permission.objects.readScope authors unit / unit_and_below, proves the owner-match widens, cross-BU stays isolated owner-anchor-and-bulk-writes -> permission.objects.modifyAllRecords member denied the transfer, privileged caller allowed semantic-roles-served -> object.highlightFields/.stageField/.fieldGroups all three survive defineStack -> artifact -> registry -> REST verbatim EIGHT DO NOT — registered with an honest blockedReason, the shape the registry already used for permission-set-projection: two guard properties already bound to a sibling proof (one entry carries one `proof` ref); one needs the `app` type to be governed; two are runtime principal-resolution invariants; two act on stack-level roles/sharingRules; one is a breadth guard over all of ADR-0090. One deliberate non-binding worth naming: owner-anchor-and-bulk-writes binds modifyAllRecords, NOT the sibling allowTransfer — the proof only mentions allowTransfer in a comment and never authors it. Binding a property a proof does not exercise is the same false comfort as a preview renderer standing in for a runtime consumer, which is what #3686 spent its length unwinding. Verified the bound proofs run: the only skipIf among them covers the attachments enterprise cross-tenant block, not the FILES_DISABLED assertion. The gate now runs with ZERO warnings — the orphan list joins the stale-evidence list at empty, so both mean something again. The README ratchet table was itself stale (5 listed, 10 bound); now complete, with the unbound set and its reasons. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ajwvrmd1hDC9RBofYBhGuR --- .changeset/liveness-register-orphan-proofs.md | 54 ++++ packages/spec/liveness/README.md | 54 +++- packages/spec/liveness/object.json | 4 + packages/spec/liveness/permission.json | 3 + .../spec/scripts/liveness/proof-registry.mts | 235 ++++++++++++++++++ .../scripts/liveness/proof-registry.test.ts | 10 + 6 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 .changeset/liveness-register-orphan-proofs.md diff --git a/.changeset/liveness-register-orphan-proofs.md b/.changeset/liveness-register-orphan-proofs.md new file mode 100644 index 0000000000..ee0114f458 --- /dev/null +++ b/.changeset/liveness-register-orphan-proofs.md @@ -0,0 +1,54 @@ +--- +"@objectstack/spec": patch +--- + +feat(spec): register the 13 orphan dogfood proofs — five advance the ADR-0054 ratchet, eight say why they can't + +The gate flagged 13 `@proof:` tags under `packages/qa/dogfood/test/**` that no +class in `proof-registry.mts` claimed. Silencing that warning is trivial and +worthless; the useful question is the one ADR-0054 §3 actually asks: **is there +an authorable property whose `live` status this proof gates?** Each of the 13 was +re-read against that. + +**Five had one, and are now BOUND** — a `live` classification on these entries +requires its proof, so the ratchet advances from 10 bound paths to 17: + +| Proof | Now gates | Why it qualifies | +|---|---|---| +| `attachments-permission-matrix` | `object.enable.files` | the #2727 opt-in gate proven in BOTH directions — the fixture carries a deliberate non-declaring object (`att_nofiles`) that must be refused 403 FILES_DISABLED | +| `showcase-d3-d4-capabilities` | `permission.rowLevelSecurity.check` | authors `check: 'owner == current_user.email'` and proves the write POST-image is validated (distinct from `using`, which filters the pre-image) | +| `showcase-scope-depth` | `permission.objects.readScope` | authors `unit` / `unit_and_below` profiles and proves the owner-match widens, with cross-BU still isolated | +| `owner-anchor-and-bulk-writes` | `permission.objects.modifyAllRecords` | member denied the transfer, privileged caller allowed — both directions | +| `semantic-roles-served` | `object.highlightFields`, `.stageField`, `.fieldGroups` | asserts all three survive defineStack → artifact → registry → REST verbatim (incl. `stageField: false` as a strict false) | + +**Eight do not, and record why** rather than faking a binding — the shape the +registry already used for `permission-set-projection`: + +- `flow-runas-schedule` and `showcase-scope-depth-fallback` guard properties + (`flow.runAs`, `permission.objects.readScope`) that are *already* bound to a + sibling proof. A ledger entry carries one `proof` ref, so a second gate on the + same property is not representable — they run unconditionally instead. +- `me-apps-and-everyone-baseline` enforces `app.requiredPermissions` / + `app.tabPermissions`; `app` is not a governed type yet. Bind when it lands. +- `showcase-agent-intersection` / `showcase-agent-scope-ceiling` guard runtime + principal-resolution invariants (`onBehalfOf`, OAuth scope → ceiling set), not + authorable metadata. +- `showcase-bu-hierarchy-sharing` / `showcase-declarative-rbac-seeding` act on + stack-level `roles`/`sharingRules`, not a per-type property surface. +- `showcase-permission-zoo` is a breadth guard over the whole ADR-0090 surface; + binding it to any one entry would misrepresent both. + +**One deliberate non-binding worth naming.** `owner-anchor-and-bulk-writes` binds +`modifyAllRecords`, not the sibling `allowTransfer` — the proof only *mentions* +`allowTransfer` in a comment and never authors it. Binding a property a proof +does not exercise is the same false comfort as a preview renderer standing in for +a runtime consumer, which is the error this ledger spent #3686 unwinding. + +Also verified the bound proofs actually run: the only `skipIf` among them covers +`attachments-permission-matrix`'s enterprise cross-tenant block, not the +FILES_DISABLED assertion the binding rests on. + +The gate now runs with **zero warnings** — the orphan list joins the +stale-evidence list at empty, so both mean something again. The ledger README's +ratchet table was itself stale (5 classes listed, 10 bound) and is now complete, +with the unbound set and its reasons alongside. diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 139dc86777..ad97648a38 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -214,14 +214,52 @@ proof gets wired in. once it has *both* a runtime proof *and* a governed ledger entry to carry it — the binding lands one class at a time (ADR-0054 §3), never as a big-bang backfill. -| High-risk class | Bound? | Ledger binding | Proof | -|---|---|---|---| -| Field types | ✅ enforced | `field.type` | `field-zoo-roundtrip.dogfood.test.ts#field-type-roundtrip` | -| RLS / sharing | ✅ enforced | `permission.rowLevelSecurity.using` | `rls-fixture.dogfood.test.ts#rls-by-id-write` | -| Master-detail controlled-by-parent | ✅ enforced | `object.sharingModel` | `controlled-by-parent.dogfood.test.ts#cbp-controlled-by-parent` | -| Flow nodes | ✅ enforced | `flow.nodes.type` | `flow-node.dogfood.test.ts#flow-node-execution` | -| Analytics dims/measures | ✅ enforced | `dataset.dimensions.dateGranularity` | `analytics-timezone.dogfood.test.ts#analytics-tz-bucketing` | -| Form layout/section/widget | ⛔ pending | — | none yet (`view` is governed as of #2998 Track B, so `view.form.*` can carry the binding — the dogfood proof is the missing half) | +`proof-registry.mts` is the source of truth; the tables below mirror it. + +**Bound — a `live` classification on these entries REQUIRES its proof:** + +| High-risk class | Ledger binding | Proof (`packages/qa/dogfood/test/…`) | +|---|---|---| +| Field types | `field.type` | `field-zoo-roundtrip#field-type-roundtrip` | +| RLS / sharing (pre-image) | `permission.rowLevelSecurity.using` | `rls-fixture#rls-by-id-write` | +| RLS `check` (post-image) | `permission.rowLevelSecurity.check` | `showcase-d3-d4-capabilities` | +| Master-detail controlled-by-parent | `object.sharingModel` | `controlled-by-parent#cbp-controlled-by-parent` | +| Scope-depth read grants | `permission.objects.readScope` | `showcase-scope-depth` | +| Ownership anchor + bulk writes | `permission.objects.modifyAllRecords` | `owner-anchor-and-bulk-writes` | +| Delegation of duty | `position.delegatable` | `delegation-of-duty#delegation-of-duty` | +| Static readonly write | `field.readonly` | `showcase-static-readonly#readonly-static-write` | +| Attachments capability gate | `object.enable.files` | `attachments-permission-matrix` | +| Analytics dims/measures | `dataset.dimensions.dateGranularity` | `analytics-timezone#analytics-tz-bucketing` | +| Flow nodes | `flow.nodes.type` | `flow-node#flow-node-execution` | +| Flow runAs identity | `flow.runAs` | `flow-runas#flow-runas-identity` | +| Data lifecycle (ADR-0057) | `object.lifecycle` | `storage-growth#adr0057-lifecycle-bounded-growth` | +| Webhook materialization | `webhook.object` | `webhook-materialization#webhook-materialization` | +| Object semantic roles (ADR-0085) | `object.highlightFields`, `.stageField`, `.fieldGroups` | `semantic-roles#semantic-roles-served` | + +**Registered but unbound.** A proof with no authorable property to ratchet is +still registered — otherwise its `@proof:` tag reads as an orphan — and records +*why* rather than faking a binding: + +| Proof | Why unbound | +|---|---| +| `form-widget-resolution` | no proof written yet (ADR-0054 Phase 2); `view.form.*` is governed and can carry it | +| `permission-set-projection` | a storage invariant (ADR-0094), not an authorable property | +| `flow-runas-schedule` | guards `flow.runAs`, already bound to `flow-runas-identity` — one entry carries one `proof` | +| `showcase-scope-depth-fallback` | guards `permission.objects.readScope`, already bound — this is the CLI-wiring sibling | +| `me-apps-and-everyone-baseline` | enforces `app.requiredPermissions` / `app.tabPermissions`; `app` is not governed yet | +| `showcase-agent-intersection` | a runtime principal-resolution invariant (`onBehalfOf`), not authorable | +| `showcase-agent-scope-ceiling` | the OAuth-scope → ceiling-set mapping lives in the runtime resolver | +| `showcase-bu-hierarchy-sharing` | stack-level `sharingRules`, not a governed per-type property | +| `showcase-declarative-rbac-seeding` | stack-level `roles`/`sharingRules` seeding, same shape | +| `showcase-permission-zoo` | a breadth guard over the whole ADR-0090 surface, not a single-property gate | + +Two habits this table is meant to enforce. **Register every proof**, so the +orphan warning stays empty and means something. **Bind only what the proof +actually authors** — `owner-anchor-and-bulk-writes` binds `modifyAllRecords`, +which it exercises in both directions, and *not* the sibling `allowTransfer`, +which it only mentions in a comment. Binding a property a proof does not +exercise is the same false comfort as a preview renderer standing in for a +runtime consumer. To bind a pending class: add its dogfood proof + `@proof:` tag, set `bound: true` and its `ledgerBindings` in `proof-registry.mts`, add the `proof` to the ledger entry, and diff --git a/packages/spec/liveness/object.json b/packages/spec/liveness/object.json index 0edc63b130..e232160289 100644 --- a/packages/spec/liveness/object.json +++ b/packages/spec/liveness/object.json @@ -37,14 +37,17 @@ }, "highlightFields": { "status": "live", + "proof": "packages/qa/dogfood/test/semantic-roles.dogfood.test.ts#semantic-roles-served", "note": "ADR-0085 semantic role (renamed from compactLayout): objectui default list/grid columns (ObjectGrid/ObjectView/InterfaceListPage), child-record previews (first entry), record:details auto layout, detail highlight strip (first 4)." }, "stageField": { "status": "live", + "proof": "packages/qa/dogfood/test/semantic-roles.dogfood.test.ts#semantic-roles-served", "note": "ADR-0085 semantic role: objectui plugin-detail detectStatusField drives the record:path stepper; string names the lifecycle field, false suppresses heuristic stage detection." }, "fieldGroups": { "status": "live", + "proof": "packages/qa/dogfood/test/semantic-roles.dogfood.test.ts#semantic-roles-served", "note": "objectui form layout order." }, "listViews": { @@ -167,6 +170,7 @@ }, "files": { "status": "live", + "proof": "packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts#attachments-permission-matrix", "evidence": "packages/plugins/plugin-audit/src/audit-writers.ts (enforceFilesCapability beforeInsert hook: sys_attachment rows may only target objects declaring enable.files true — 403 FILES_DISABLED otherwise); objectui RecordAttachmentsPanel renders the record Attachments surface (upload/list/download/delete) when the flag is true. Opt-in — spec default false (#2727)." }, "feeds": { diff --git a/packages/spec/liveness/permission.json b/packages/spec/liveness/permission.json index e886e03ca5..0dd64eee80 100644 --- a/packages/spec/liveness/permission.json +++ b/packages/spec/liveness/permission.json @@ -75,10 +75,12 @@ }, "modifyAllRecords": { "status": "live", + "proof": "packages/qa/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts#owner-anchor-and-bulk-writes", "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts:60" }, "readScope": { "status": "live", + "proof": "packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts#showcase-scope-depth", "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts (getEffectiveScope) + packages/plugins/plugin-sharing/src/sharing-service.ts (owner-match widened by readScope; hierarchy values delegated to IHierarchyScopeResolver)", "note": "ADR-0057 D1 — read access DEPTH (own/own_and_reports/unit/unit_and_below/org). own/org enforced in open edition; hierarchy values via the enterprise hierarchy-scope-resolver (fail-closed to owner-only when absent; defineStack requires 'hierarchy-security'). Proven: packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts." }, @@ -142,6 +144,7 @@ }, "check": { "status": "live", + "proof": "packages/qa/dogfood/test/showcase-d3-d4-capabilities.dogfood.test.ts#showcase-d3-d4-capabilities", "evidence": "packages/plugins/plugin-security/src/rls-compiler.ts" }, "positions": { diff --git a/packages/spec/scripts/liveness/proof-registry.mts b/packages/spec/scripts/liveness/proof-registry.mts index 2b03aaad7c..18d8dd6c9d 100644 --- a/packages/spec/scripts/liveness/proof-registry.mts +++ b/packages/spec/scripts/liveness/proof-registry.mts @@ -203,6 +203,241 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ 'projection is a storage invariant (ADR-0094), not an authorable spec property — no ledger ' + 'entry to ratchet; the proof runs unconditionally in the dogfood suite instead.', }, + + // ───────────────────────────────────────────────────────────────────────── + // Proofs that existed in the dogfood tree but were never registered here — + // the gate reported them as 13 orphan `@proof:` tags. Registering silences + // that warning, but silence was never the goal: each was re-read to ask "is + // there an authorable property whose `live` status this proof actually + // gates?" Five had one and are BOUND (the ratchet advances). Eight do not, + // and say why rather than faking a binding. + // ───────────────────────────────────────────────────────────────────────── + + { + id: 'attachments-capability', + label: 'Attachments capability gate', + summary: + '`enable.files` is the #2727 OPT-IN gate for attachments, enforced server-side: a sys_attachment ' + + 'targeting an object that does not declare it is refused 403 FILES_DISABLED. Proven in both ' + + 'directions over the real presigned three-step upload — declaring objects accept, the ' + + '`att_nofiles` fixture is refused — alongside the non-admin permission matrix (uploader gate, ' + + 'parent read-visibility, server-stamped uploaded_by, tenant isolation).', + proofId: 'attachments-permission-matrix', + proofRef: 'packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts#attachments-permission-matrix', + bound: true, + // A capability flag that only HIDES a UI panel is the false-compliance + // shape ADR-0049 closes; the fixture carries a deliberate non-declaring + // object so the negative direction is pinned, not assumed. The file's one + // `describe.skipIf` covers the ENTERPRISE cross-tenant block only — the + // FILES_DISABLED assertion this binding rests on runs unconditionally. + ledgerBindings: [{ type: 'object', path: 'enable.files' }], + }, + { + id: 'rls-check-post-image', + label: 'RLS `check` post-image validation', + summary: + 'an RLS `check` clause validates the write POST-IMAGE (ADR-0058 D4) — a contributor who owns an ' + + 'invoice cannot reassign it to a different owner. Distinct from `using`, which filters the ' + + 'pre-image: a policy that compiled but was never applied to the resulting row would leave ' + + 'every "you may not change this away from yourself" rule decorative.', + proofId: 'showcase-d3-d4-capabilities', + proofRef: 'packages/qa/dogfood/test/showcase-d3-d4-capabilities.dogfood.test.ts#showcase-d3-d4-capabilities', + bound: true, + // The same file also pins the ADR-0058 D3 compound sharing `condition` + // (`&&`), which silently skipped the AND before #1887 — but stack-level + // sharing rules are not a governed metadata type, so only `check` binds. + ledgerBindings: [{ type: 'permission', path: 'rowLevelSecurity.check' }], + }, + { + id: 'scope-depth-read', + label: 'Scope-depth read grants', + summary: + "`readScope` widens the owner-match on an owner-scoped (private) object: `unit` → the caller's " + + 'business-unit co-members, `unit_and_below` → that BU plus every descendant (BFS). Sharing ' + + 'still widens on top and cross-BU stays isolated (ADR-0057 D1). Hierarchy resolution is an ' + + 'enterprise capability, so the proof registers a reference resolver to pin the seam + ' + + 'contract; the open edition fails closed to owner-only.', + proofId: 'showcase-scope-depth', + proofRef: 'packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts#showcase-scope-depth', + bound: true, + // An access-WIDENING grant: "declared but not applied" reads as a working + // restriction, so the failure is silent in the safe-looking direction. + ledgerBindings: [{ type: 'permission', path: 'objects.readScope' }], + }, + { + id: 'ownership-anchor-writes', + label: 'Ownership anchor + bulk write scoping', + summary: + '`owner_id` is system-managed for non-privileged writers (#3004): a member can neither plant a ' + + 'record under another name (insert forge) nor move/disown one (update transfer) — that needs ' + + '`modifyAllRecords` (which implies transfer). Paired with #2982: bulk `update({multi:true})` ' + + '/ delete used to rebuild the driver AST AFTER the middleware chain, so owner scoping never ' + + 'reached them and a member\'s bulk write hit every matching row, peers included.', + proofId: 'owner-anchor-and-bulk-writes', + proofRef: 'packages/qa/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts#owner-anchor-and-bulk-writes', + bound: true, + // Bound to `modifyAllRecords` — the grant the proof actually authors and + // exercises in BOTH directions (member denied, privileged caller allowed). + // NOT to the sibling `allowTransfer`: the proof never authors it, and + // binding a property a proof does not exercise is the same false comfort + // as a preview renderer standing in for a runtime consumer. + ledgerBindings: [{ type: 'permission', path: 'objects.modifyAllRecords' }], + }, + { + id: 'semantic-roles', + label: 'Object semantic roles (ADR-0085)', + summary: + '`highlightFields` / `stageField` / `fieldGroups` survive the pipeline a real renderer consumes ' + + '— defineStack → artifact → registry → REST serialization — served verbatim over HTTP. Parse-' + + 'level unit tests say nothing about that path, and it is exactly where the pre-ADR-0085 ' + + 'dialects silently died (spec-authored `defaultExpanded` never reached the form). A serializer ' + + 'whitelist or a boot-cached merge can drop a key with no static check noticing.', + proofId: 'semantic-roles-served', + proofRef: 'packages/qa/dogfood/test/semantic-roles.dogfood.test.ts#semantic-roles-served', + bound: true, + // One proof, three properties: the test asserts each is served verbatim + // (incl. `stageField: false` surviving as a strict false, not dropped). + ledgerBindings: [ + { type: 'object', path: 'highlightFields' }, + { type: 'object', path: 'stageField' }, + { type: 'object', path: 'fieldGroups' }, + ], + }, + + // ── Registered, honestly unbound ──────────────────────────────────────── + + { + id: 'flow-runas-userless', + label: 'Flow runAs — the user-less run', + summary: + "a run with no trigger user under an effective `runAs:'user'` resolves no identity, so its CRUD " + + 'nodes present no ObjectQL context and the security middleware SKIPS — the run would execute ' + + 'UNSCOPED. Until #3760 that is what happened, and this file pinned it. `runAs:\'user\'` is ' + + 'access-NARROWING, so failing to resolve it must never resolve to a grant: the run is refused.', + proofId: 'flow-runas-schedule', + proofRef: 'packages/qa/dogfood/test/flow-runas-schedule.dogfood.test.ts#flow-runas-schedule', + bound: false, + ledgerBindings: [], + blockedReason: + "guards `flow.runAs`, which is already bound to `flow-runas-identity` — a ledger entry carries " + + 'one `proof` ref, so the boundary-case sibling cannot also bind. It runs unconditionally in ' + + 'the dogfood suite.', + }, + { + id: 'scope-depth-cli-fallback', + label: 'Scope-depth via the CLI default-profile wiring', + summary: + 'the app declares its default profile in METADATA, the CLI passes only its NAME as ' + + '`fallbackPermissionSet`, and the SecurityPlugin resolves the full set (incl. `readScope`) ' + + 'from sys_permission_set at request time. The artifact-serve path used to drop `permissions[]` ' + + 'from the stack config, so the fallback silently degraded to the built-in owner-only ' + + '`member_default` and a grant-less user never got the declared widening.', + proofId: 'showcase-scope-depth-fallback', + proofRef: 'packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts#showcase-scope-depth-fallback', + bound: false, + ledgerBindings: [], + blockedReason: + 'guards the same `permission.objects.readScope` entry as `showcase-scope-depth`, over the CLI ' + + 'wiring rather than an injected bootstrap set — one entry, one `proof` ref, so it cannot also ' + + 'bind.', + }, + { + id: 'app-tab-permissions', + label: 'GET /me/apps + anchor-bindable baseline', + summary: + '/me/apps used to read `metadata.list(\'app\')` while stack apps live in the ENGINE REGISTRY, ' + + 'returning [] for every principal — leaving `tabPermissions` and `AppSchema.requiredPermissions` ' + + 'with no enforced consumer (#2752). Paired with #2753: the `member_default` baseline carried an ' + + 'anchor-forbidden `allowDelete` on `*`, so the bootstrap refused to bind it to `everyone` on ' + + 'every boot and the baseline flowed only through the fallback channel ADR-0090 D5 rejected.', + proofId: 'me-apps-and-everyone-baseline', + proofRef: 'packages/qa/dogfood/test/me-apps-and-everyone-baseline.dogfood.test.ts#me-apps-and-everyone-baseline', + bound: false, + ledgerBindings: [], + blockedReason: + 'the properties it enforces (`app.requiredPermissions`, `app.tabPermissions`) belong to the ' + + '`app` type, which is not yet governed by the ledger (rollout). Bind when `app` lands.', + }, + { + id: 'agent-delegator-intersection', + label: 'ADR-0090 D10 agent/delegator intersection', + summary: + 'the reconstructed delegator context substitutes correctly into a real compiled ' + + '`owner_id = current_user.id` policy, and the intersection STRIPS an agent\'s View-All when the ' + + 'delegator lacks it — an agent may not see what the user it stands in for cannot.', + proofId: 'showcase-agent-intersection', + proofRef: 'packages/qa/dogfood/test/showcase-agent-intersection.dogfood.test.ts#showcase-agent-intersection', + bound: false, + ledgerBindings: [], + blockedReason: + 'the intersection is a runtime principal-resolution invariant (principalKind `agent` + ' + + '`onBehalfOf`), not an authorable property — no `agent.*` field whose `live` status it gates.', + }, + { + id: 'agent-scope-ceiling', + label: 'ADR-0090 D10 OAuth-scope agent ceiling', + summary: + 'an MCP OAuth scope becomes a real data-layer boundary, not a tool-surface hint: a `data:read` ' + + 'agent acting for a user who CAN write is blocked at the data layer, while `data:write` for the ' + + 'same user is allowed — the intersection only ever narrows.', + proofId: 'showcase-agent-scope-ceiling', + proofRef: 'packages/qa/dogfood/test/showcase-agent-scope-ceiling.dogfood.test.ts#showcase-agent-scope-ceiling', + bound: false, + ledgerBindings: [], + blockedReason: + 'the scope → ceiling-set mapping (`data:read` → mcp_agent_data_read) lives in the runtime ' + + 'execution-context resolver, not in authorable metadata — no ledger entry to ratchet.', + }, + { + id: 'bu-hierarchy-sharing', + label: 'Business-unit subtree sharing', + summary: + "a sharing rule whose recipient is a BUSINESS UNIT widens access DOWN the tree: the unit's members " + + 'AND every descendant unit\'s members gain access via sys_business_unit (BFS). The honest ' + + 're-homing of the broken `unit_and_subordinates` (sys_position.parent never existed) onto the ' + + 'working BU tree — the rule materialises sys_record_share rows and a non-owner in the subtree ' + + 'can then read a private record.', + proofId: 'showcase-bu-hierarchy-sharing', + proofRef: 'packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts#showcase-bu-hierarchy-sharing', + bound: false, + ledgerBindings: [], + blockedReason: + 'sharing rules are authored at STACK level (`sharingRules`), which is not a governed metadata ' + + 'type — the ledger governs per-type property surfaces, and there is no `permission.*` entry ' + + 'for the rule\'s recipient kind.', + }, + { + id: 'declarative-rbac-seeding', + label: 'Declarative RBAC seeding', + summary: + 'stack-declared `roles` + `sharingRules` are seeded into sys_position / sys_sharing_rule at boot, ' + + 'so they stop being decorative — #2077 reported booting the showcase yielded a count of 0 for ' + + 'both. Also pins the spec→runtime translation.', + proofId: 'showcase-declarative-rbac-seeding', + proofRef: 'packages/qa/dogfood/test/showcase-declarative-rbac-seeding.dogfood.test.ts#showcase-declarative-rbac-seeding', + bound: false, + ledgerBindings: [], + blockedReason: + 'seeding acts on STACK-level `roles`/`sharingRules` collections, not on a per-type authorable ' + + 'property — same shape as bu-hierarchy-sharing.', + }, + { + id: 'permission-model-zoo', + label: 'ADR-0090 permission-model zoo', + summary: + 'the showcase declares the FULL authoring surface (positions, CRUD/FLS/RLS sets, org-depth, VAMA, ' + + 'system permissions, everyone/guest capability, adminScope, a seeded sys_business_unit tree, ' + + 'BU-subtree sharing) and the SERVED runtime enforces it rather than merely storing it. Each ' + + 'block names the ADR-0090 decision it guards.', + proofId: 'showcase-permission-zoo', + proofRef: 'packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts#showcase-permission-zoo', + bound: false, + ledgerBindings: [], + blockedReason: + 'a BREADTH guard over the whole ADR-0090 surface, not a single-property gate — binding it to any ' + + 'one entry would misrepresent both what it covers and what that entry is proven by. The ' + + 'per-property bindings above carve out the parts that do have a single owner.', + }, ]; /** Bound ledger paths → the class that binds them. Key: `/`. */ diff --git a/packages/spec/scripts/liveness/proof-registry.test.ts b/packages/spec/scripts/liveness/proof-registry.test.ts index 33a93bcbb1..aaebbd38ed 100644 --- a/packages/spec/scripts/liveness/proof-registry.test.ts +++ b/packages/spec/scripts/liveness/proof-registry.test.ts @@ -113,6 +113,16 @@ describe('registry invariants', () => { 'position/delegatable', 'object/lifecycle', 'webhook/object', + // Bound when the 13 orphan `@proof:` tags were registered — the five + // classes that had an authorable property whose `live` status they + // actually gate (the other eight record a blockedReason instead). + 'object/enable.files', + 'object/highlightFields', + 'object/stageField', + 'object/fieldGroups', + 'permission/rowLevelSecurity.check', + 'permission/objects.readScope', + 'permission/objects.modifyAllRecords', ].sort(), ); });