diff --git a/packages/qa/dogfood/test/authz-conformance.matrix.ts b/packages/qa/dogfood/test/authz-conformance.matrix.ts index b27963f37ca..2b194ece503 100644 --- a/packages/qa/dogfood/test/authz-conformance.matrix.ts +++ b/packages/qa/dogfood/test/authz-conformance.matrix.ts @@ -158,10 +158,10 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // ── Enforced + end-to-end proven ─────────────────────────────────────── { id: 'rls-read', summary: 'RLS `using` read filter', state: 'enforced', enforcement: 'plugin-security/security-plugin.ts computeRlsFilter (AND-injected)', proof: 'rls-fixture.dogfood.test.ts' }, - { id: 'rls-by-id-write', summary: 'by-id write enforcement (#1994)', state: 'enforced', - enforcement: 'plugin-security/security-plugin.ts step 2.7 pre-image re-read, composed with computeRlsFilter\'s write-scope DERIVATION (#7665): when no update/delete-class policy applies, the write class is compiled from the caller\'s SELECT narrowing, so the pre-image gate has a predicate to enforce', + { id: 'rls-by-id-write', summary: 'by-id write enforcement', state: 'enforced', + enforcement: 'plugin-security/security-plugin.ts step 2.7 pre-image re-read, composed with computeRlsFilter\'s write-scope DERIVATION: when no update/delete-class policy applies, the write class is compiled from the caller\'s SELECT narrowing, so the pre-image gate has a predicate to enforce', proof: 'rls-fixture.dogfood.test.ts', - note: '[#7685] Re-verified as `enforced`, both halves of the enforcement named because ONLY BOTH hold it. The pre-image re-read alone is a no-op under select-only authoring — that was #7665, and it is why the site had to be re-cited here: ablating the derivation while leaving the pre-image re-read fully in place turns 16 of 20 probed showcase objects into `rls-hole`. The proof is NOT vacuous for this row despite sharing `rls-fixture.dogfood.test.ts` with `rls-read`: since #7665/PR #7792 that file carries a dedicated select-only block whose member set grants FULL CRUD on `rls_note` (so a refusal is the record gate, never the object gate) asserting the by-id PATCH is refused with the row unchanged, plus that an in-scope write still lands. Second, independent measurement: `objectstack verify --rls`, whose probe persona reaches this class since #7685 — 20/23 showcase and 6/6 crm objects PROVEN, 0 holes, and 16 holes under the same ablation.' }, + note: 'Re-verified as `enforced`, both halves of the enforcement named because ONLY BOTH hold it. The pre-image re-read alone is a no-op under select-only authoring — that was the hole through which a contributor PATCHed records it could not read, and it is why the site had to be re-cited here: ablating the derivation while leaving the pre-image re-read fully in place turns 16 of 20 probed showcase objects into `rls-hole`. The proof is NOT vacuous for this row despite sharing `rls-fixture.dogfood.test.ts` with `rls-read`: since the fix that derives a missing write scope from the select narrowing, that file carries a dedicated select-only block whose member set grants FULL CRUD on `rls_note` (so a refusal is the record gate, never the object gate) asserting the by-id PATCH is refused with the row unchanged, plus that an in-scope write still lands. Second, independent measurement: `objectstack verify --rls`, whose probe persona holds object read+edit narrowed by select-only RLS and so reaches this class — 20/23 showcase and 6/6 crm objects PROVEN, 0 holes, and 16 holes under the same ablation.' }, { id: 'rls-write-check', summary: 'RLS `check` write post-image validation (ADR-0058 D4)', state: 'enforced', enforcement: 'plugin-security/security-plugin.ts step 3.6 — compileCelToFilter + matchesFilterCondition against the post-image (fail-closed)', note: 'Unit-proven in plugin-security/security-plugin.test.ts (RLS check enforcement); see ADR-0058 D7 ledger.' }, @@ -171,15 +171,15 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ enforcement: 'plugin-sharing/sharing-service.ts (read model + canEdit)', proof: 'showcase-public-read-owd.dogfood.test.ts' }, { id: 'controlled-by-parent', summary: 'master-detail controlled_by_parent', state: 'enforced', enforcement: 'plugin-security/security-plugin.ts computeControlledByParentFilter + assertControlledByParentWrite', proof: 'controlled-by-parent.dogfood.test.ts', - note: '[#7685] Re-verified as `enforced` on its OWN evidence, not by association with `rls-by-id-write`. The cited proof is DEDICATED and non-vacuous: `fixtures/cbp-fixture.ts` grants the member full CRUD on BOTH `cbp_account` and `cbp_note`, so every refusal it asserts is the derived record gate rather than the object gate, and the detail carries no authored RLS at all — access is derived from the owner-scoped master. It asserts the derived READ denial, the derived by-id WRITE denial with the row unchanged as ground truth, and that a note under a master the member owns stays readable AND writable (so the guard is not over-blocking). Second measurement: `verify --rls` probes `showcase_invoice_line` — a real `controlled_by_parent` detail — as `rls-consistent`, and it flips to `rls-hole` when the #7665 write-scope derivation its master depends on is ablated.' }, + note: 'Re-verified as `enforced` on its OWN evidence, not by association with `rls-by-id-write`. The cited proof is DEDICATED and non-vacuous: `fixtures/cbp-fixture.ts` grants the member full CRUD on BOTH `cbp_account` and `cbp_note`, so every refusal it asserts is the derived record gate rather than the object gate, and the detail carries no authored RLS at all — access is derived from the owner-scoped master. It asserts the derived READ denial, the derived by-id WRITE denial with the row unchanged as ground truth, and that a note under a master the member owns stays readable AND writable (so the guard is not over-blocking). Second measurement: `verify --rls` probes `showcase_invoice_line` — a real `controlled_by_parent` detail — as `rls-consistent`, and it flips to `rls-hole` when the select-derived write-scope derivation its master depends on is ablated.' }, { id: 'multi-tenant', summary: 'organization isolation', state: 'enforced', enforcement: '@objectstack/organizations (enterprise) + Layer 0 tenant wall (plugin-security/tenant-layer.ts, AND-composed ahead of business RLS — ADR-0095 D1)', proof: 'rls-multitenant.dogfood.test.ts' }, - { id: 'multi-tenant-write-postimage', summary: 'Layer 0 tenant post-image check on INSERT + UPDATE (#2937 / Finding 1 — a forged OR re-pointed organization_id cannot cross the tenant wall)', state: 'enforced', + { id: 'multi-tenant-write-postimage', summary: 'Layer 0 tenant post-image check on INSERT + UPDATE (forged INSERT / Finding 1 re-point — a forged OR re-pointed organization_id cannot cross the tenant wall)', state: 'enforced', enforcement: 'plugin-security/security-plugin.ts step 3.7 — computeWriteTenantCheckFilter (reuses computeLayeredRlsFilter\'s Layer 0) matched against the write post-image (fail-closed) for BOTH insert and update; enterprise auto-stamp authoritatively overwrites a user-context organization_id (@objectstack/organizations Middleware A)', - note: 'INSERT has no pre-image and UPDATE\'s pre-image (step 2.7) validates only the OLD organization_id, so the AND-composed Layer 0 wall never inspected the NEW value: a member could INSERT a forged cross-tenant organization_id (#2937) or UPDATE a row to RE-POINT it into a victim tenant (Finding 1, BLOCKER). A supplied cross-tenant organization_id is now DENIED on both paths — organization_id is effectively immutable in non-platform user contexts (platform-admin posture on a posture-permitting object + single-mode exempt, same rule as the read side). Unit-proven in plugin-security/authz-matrix-gate.test.ts ([#2937] insert + [Finding 1 / #2937] update post-image tenant guard). Multi-org is enterprise-only so it is not in the open-core dogfood boot; see ADR-0095 D1.' }, + note: 'INSERT has no pre-image and UPDATE\'s pre-image (step 2.7) validates only the OLD organization_id, so the AND-composed Layer 0 wall never inspected the NEW value: a member could INSERT a forged cross-tenant organization_id or UPDATE a row to RE-POINT it into a victim tenant (Finding 1, BLOCKER). A supplied cross-tenant organization_id is now DENIED on both paths — organization_id is effectively immutable in non-platform user contexts (platform-admin posture on a posture-permitting object + single-mode exempt, same rule as the read side). Unit-proven in plugin-security/authz-matrix-gate.test.ts (the "Layer 0 insert post-image tenant guard" suite + the Finding 1 "Layer 0 update post-image tenant guard (cross-tenant re-point)" suite). Multi-org is enterprise-only so it is not in the open-core dogfood boot; see ADR-0095 D1.' }, { id: 'multi-tenant-exemption-posture', summary: 'Layer 0 cross-tenant exemption requires the PLATFORM_ADMIN posture (Finding 2 — org_admin does not cross the wall)', state: 'enforced', - enforcement: 'plugin-security/security-plugin.ts computeLayeredRlsFilter reads the carried ctx.posture rung (ADR-0099 D1 / #2956) to gate the tenant-layer.ts Layer 0 exemption — PLATFORM_ADMIN crosses, everything below is walled; the hasPlatformAdminPosture capability probe is the resolver-less fallback; the superuser bit (viewAllRecords/modifyAllRecords) governs only the Layer 1 business-RLS short-circuit', - note: 'An organization_admin holds the superuser bit via its `*` wildcard, so it used to also get the Layer 0 exemption and read/write EVERY tenant\'s rows on private tenant objects. Crossing now requires the carried PLATFORM_ADMIN rung (ADR-0099 P1), which derives only from an unscoped admin_full_access grant — org_admin resolves to TENANT_ADMIN and a scoped grant / piecemeal platform capability to MEMBER, so all are walled to their own org (SECURITY NARROWING; a true platform admin still crosses, the better-auth carve-out is untouched). Before P1 the gate keyed on a platform-exclusive capability probe, which a scoped admin_full_access grant or a piecemeal studio.access grant could satisfy — the divergence class the equivalence gate pinned and P1 closed (invariant I1: TENANT_ADMIN never crosses). Unit-proven in plugin-security/authz-matrix-gate.test.ts ([Finding 2 / #2937] platform-posture exemption + "ADR-0099 P1 — Layer 0 exemption reads the carried rung").' }, + enforcement: 'plugin-security/security-plugin.ts computeLayeredRlsFilter reads the carried ctx.posture rung (ADR-0099 D1) to gate the tenant-layer.ts Layer 0 exemption — PLATFORM_ADMIN crosses, everything below is walled; the hasPlatformAdminPosture capability probe is the resolver-less fallback; the superuser bit (viewAllRecords/modifyAllRecords) governs only the Layer 1 business-RLS short-circuit', + note: 'An organization_admin holds the superuser bit via its `*` wildcard, so it used to also get the Layer 0 exemption and read/write EVERY tenant\'s rows on private tenant objects. Crossing now requires the carried PLATFORM_ADMIN rung (ADR-0099 P1), which derives only from an unscoped admin_full_access grant — org_admin resolves to TENANT_ADMIN and a scoped grant / piecemeal platform capability to MEMBER, so all are walled to their own org (SECURITY NARROWING; a true platform admin still crosses, the better-auth carve-out is untouched). Before P1 the gate keyed on a platform-exclusive capability probe, which a scoped admin_full_access grant or a piecemeal studio.access grant could satisfy — the divergence class the equivalence gate pinned and P1 closed (invariant I1: TENANT_ADMIN never crosses). Unit-proven in plugin-security/authz-matrix-gate.test.ts (the Finding 2 "Layer 0 cross-tenant exemption requires the platform posture" suite + "ADR-0099 P1 — Layer 0 exemption reads the carried rung").' }, // ── ADR-0105 — group tenancy posture; organization scope as a first-class // authorization dimension. Phase 0 (F1/F2 correctness) + Phase 1 (the `group` // union wall). Multi-org is not in the open-core dogfood boot, so these are @@ -192,7 +192,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ note: 'ONE read serves both the active-org position projection and the full membership set, so the two facts cannot disagree. A transport that fails to carry the set denies under `group` rather than falling back to the active org — the wall must not depend on which surface the request arrived through. Delegated (on-behalf-of) reads resolve the DELEGATOR\'s own set (explain-engine buildContextForUser), never inherit the live principal\'s.' }, { id: 'org-write-validation', summary: 'bulk-aware organization_id write validation (ADR-0105 D5)', state: 'enforced', enforcement: 'plugin-security/security-plugin.ts step 3.7 — every SUPPLIED organization_id (single row AND bulk array) must satisfy the Layer 0 filter or the whole write is denied', - note: 'The bulk path was a genuine hole: the pre-D5 check required a non-array payload, so an INSERT of an ARRAY could carry a forged organization_id per row — the #2937 defect one call site down. STAMPING an absent value is deliberately NOT done here: it belongs to the @objectstack/organizations runtime (Middleware A), which is also what ACTIVATES every walled posture (ADR-0105 D12), so the stamper is always present wherever a wall is. Keeping the stamp there means a forged `org-scoping` registration produces NULL-org rows the wall hides — a broken deployment, not a working unlicensed one — while validation stays in plugin-security because it must hold under every posture, walled or not. Since ADR-0132 the stamper ships open as well, so this split is about WHERE each half runs and no longer about which edition ships it. Unit-proven in plugin-security/security-plugin.test.ts + authz-matrix-gate.test.ts.' }, + note: 'The bulk path was a genuine hole: the pre-D5 check required a non-array payload, so an INSERT of an ARRAY could carry a forged organization_id per row — the forged-organization_id INSERT defect one call site down. STAMPING an absent value is deliberately NOT done here: it belongs to the @objectstack/organizations runtime (Middleware A), which is also what ACTIVATES every walled posture (ADR-0105 D12), so the stamper is always present wherever a wall is. Keeping the stamp there means a forged `org-scoping` registration produces NULL-org rows the wall hides — a broken deployment, not a working unlicensed one — while validation stays in plugin-security because it must hold under every posture, walled or not. Since ADR-0132 the stamper ships open as well, so this split is about WHERE each half runs and no longer about which edition ships it. Unit-proven in plugin-security/security-plugin.test.ts + authz-matrix-gate.test.ts.' }, { id: 'authored-rls-policy-survival', summary: 'app-authored org-scoped RLS policies are never silently stripped (ADR-0105 D3 / F1)', state: 'enforced', enforcement: 'plugin-security/platform-tenant-policies.ts — collectRLSPolicies strips by PROVENANCE (identity against the shipped declaration), not by substring-matching `current_user.organization_id`; an authored policy is retained, warned about once, and fails closed at compile time', note: 'The substring match dropped ANY policy using the token, including app-authored ones — a declared security policy silently unenforced, the ADR-0049 class. getReadFilter shared the defect, so analytics/raw-SQL consumers got an UNSCOPED read. Unit-proven in plugin-security/platform-tenant-policies.test.ts + security-plugin.test.ts.' }, @@ -210,7 +210,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // were duplicate supply that this plugin no longer serves, so there is no // entry point left to gate there. The invariant is unchanged — it simply has // one fewer implementation to hold it in. - { id: 'anonymous-deny-meta', summary: 'anonymous-deny on the metadata endpoints (#2567 surface 1)', state: 'enforced', + { id: 'anonymous-deny-meta', summary: 'anonymous-deny on the metadata endpoints (uniform anonymous posture, surface 1)', state: 'enforced', enforcement: 'rest/rest-server.ts registerMetadataEndpoints guarded registrar (enforceAuth → shouldDenyAnonymous) — every /meta route inherits the gate; runtime/http-dispatcher.ts handleMetadata mirrors it for the dispatcher metadata catch-all', proof: 'showcase-anonymous-deny-surfaces.dogfood.test.ts', // [2026-08-31] The two ledger-sourced keys are NOT a new claim: they name @@ -225,7 +225,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ 'meta:rest-server.ts:registerMetadataEndpoints', 'meta:http-dispatcher.ts:handleMetadata', 'rest-family:rest-route-ledger.ts:metadata', 'dispatcher-domain:route-ledger.ts:/meta', ], - note: '#11373 — for most of this row\'s life the cited proof drove ONE anonymous `GET /meta`, so the row read as covering a surface while only its read face was exercised. The mutating doors (`_migrate-stored`, the single save, the reset, publish, rollback — six when measured, five since #12176 D3 retired the compound save) are now driven there as real HTTP: measured 2026-08-23 on the booted showcase, every mounted door answers 401 UNAUTHENTICATED in the rest-flat envelope, nothing persists, and the same URL/method/body with a session answers 403 (member) or runs the door (admin) — so the 401 is the floor and not a broken probe; the retired compound spelling has its own case there pinning 404-for-everyone, since an auth floor only speaks for a door that exists. The write half was previously pinned only in `rest/src/meta-write-door-capability-enumeration.test.ts`, which invokes `route.handler` over a `vi.fn()` transport and therefore could not show that the composed app routes a real request into the guarded registrar at all.' }, + note: 'For most of this row\'s life the cited proof drove ONE anonymous `GET /meta`, so the row read as covering a surface while only its read face was exercised. The mutating doors (`_migrate-stored`, the single save, the reset, publish, rollback — six when measured, five since the retirement of slash-bearing metadata item names un-mounted the compound save) are now driven there as real HTTP: measured 2026-08-23 on the booted showcase, every mounted door answers 401 UNAUTHENTICATED in the rest-flat envelope, nothing persists, and the same URL/method/body with a session answers 403 (member) or runs the door (admin) — so the 401 is the floor and not a broken probe; the retired compound spelling has its own case there pinning 404-for-everyone, since an auth floor only speaks for a door that exists. The write half was previously pinned only in `rest/src/meta-write-door-capability-enumeration.test.ts`, which invokes `route.handler` over a `vi.fn()` transport and therefore could not show that the composed app routes a real request into the guarded registrar at all.' }, // #5519 — the two DISPATCHER-mounted execution surfaces. `@objectstack/rest` // gated `/data` and `/meta`; these routes are mounted by a SECOND // registration path (dispatcher-plugin.ts, straight onto the host @@ -233,7 +233,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // claim above was false on them until PR #5569. The proof artifact was // silent too — #5570 is the evidence half, and these two rows are what make // the gate's removal fail CI instead of review. - { id: 'anonymous-deny-actions', summary: 'anonymous-deny on the business-action dispatch surface (#2567 surface 2 / #5519)', state: 'enforced', + { id: 'anonymous-deny-actions', summary: 'anonymous-deny on the business-action dispatch surface (uniform anonymous posture, surface 2: refused 401 before dispatch, as `/data` and `/meta` are)', state: 'enforced', enforcement: 'runtime/domains/actions.ts handleActionsRequest — shouldDenyAnonymous as the handler\'s FIRST statement, ahead of the ADR-0066 D4 requiredPermissions gate and the ADR-0104 param contract; those keep their semantics and simply run after the auth baseline, so an anonymous caller never reaches action dispatch and never learns the route\'s shape', proof: 'showcase-anonymous-deny-surfaces.dogfood.test.ts', // [2026-08-31] Same surface as the gate pin beside it, at ledger @@ -241,16 +241,16 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // gate is the single handler body's first statement), so the domain key // adds no claim the pinned gate did not already carry. covers: ['actions:domains/actions.ts:anonymous-gate', 'dispatcher-domain:route-ledger.ts:/actions'], - note: 'A `type: \'script\'` action body runs `isSystem: true` (elevated), so an ungated POST was an anonymous privilege-escalating WRITE, not merely an information leak — #5519 measured `POST /actions/showcase_task/showcase_mark_done/:id` answering 200 with the update applied. Internal dispatch is unaffected: this handler is a pure HTTP seam (the MCP `run_action` bridge enters through action-execution.invokeBusinessAction, declarative endpoints through the transport fallback seam with their own `authRequired` gate), so `authRequired: false` public endpoints stay public.' }, - { id: 'anonymous-deny-automation', summary: 'anonymous-deny on the automation/flow surface (#2567 surface 3 / #5519)', state: 'enforced', - enforcement: 'runtime/domains/automation.ts handleAutomationRequest — shouldDenyAnonymous DOMAIN-WIDE at the top, and deliberately BEFORE the isServiceServeable probe so the 401/501 difference cannot be used to fingerprint whether a deployment mounts automation; per-route capability predicates run after this floor — `manage_metadata` for the five gated flow writes (create `POST /` / update `PUT /:name` / deregister `DELETE /:name`, #10145, plus enablement `POST /:name/toggle` since the #10243 ruling of 2026-08-23, which measured that the enabled bit is not a ROW and so reaches every organization on the deployment, plus the ADR-0126 §7.1 clone `POST /:name/clone`, which registers flow metadata at environment scope exactly as create does), all selected by the ONE `isFlowAuthoringWrite` predicate, fail-closed by construction (an absent executionContext, an absent `systemPermissions` or an empty one all refuse) and answering 403 `PERMISSION_DENIED`, with only engine `isSystem` bypassing; the run-state reads (#7900) and `resume` (#3801 / #5561) carry their own separate per-route predicates, and the execution doors (trigger / execute) sit outside all of them — including `POST /trigger/:name` for a flow literally NAMED `toggle`, which the toggle arm deliberately excludes so a name cannot cost a member its run door', + note: 'A `type: \'script\'` action body runs `isSystem: true` (elevated), so an ungated POST was an anonymous privilege-escalating WRITE, not merely an information leak — before the gate, an anonymous `POST /actions/showcase_task/showcase_mark_done/:id` was measured answering 200 with the update applied. Internal dispatch is unaffected: this handler is a pure HTTP seam (the MCP `run_action` bridge enters through action-execution.invokeBusinessAction, declarative endpoints through the transport fallback seam with their own `authRequired` gate), so `authRequired: false` public endpoints stay public.' }, + { id: 'anonymous-deny-automation', summary: 'anonymous-deny on the automation/flow surface (uniform anonymous posture, surface 3: refused 401 before dispatch, as `/data` and `/meta` are)', state: 'enforced', + enforcement: 'runtime/domains/automation.ts handleAutomationRequest — shouldDenyAnonymous DOMAIN-WIDE at the top, and deliberately BEFORE the isServiceServeable probe so the 401/501 difference cannot be used to fingerprint whether a deployment mounts automation; per-route capability predicates run after this floor — `manage_metadata` for the five gated flow writes (create `POST /` / update `PUT /:name` / deregister `DELETE /:name`, the definition writes on the metadata plane, plus enablement `POST /:name/toggle` since the 2026-08-23 ruling that enablement is an authoring write, which measured that the enabled bit is not a ROW and so reaches every organization on the deployment, plus the ADR-0126 §7.1 clone `POST /:name/clone`, which registers flow metadata at environment scope exactly as create does), all selected by the ONE `isFlowAuthoringWrite` predicate, fail-closed by construction (an absent executionContext, an absent `systemPermissions` or an empty one all refuse) and answering 403 `PERMISSION_DENIED`, with only engine `isSystem` bypassing; the run-state reads (the `sys_automation_run` read grant) and `resume` (keyed on the node the run is suspended on, fail-closed for a node that declares no resumeAuthority) carry their own separate per-route predicates, and the execution doors (trigger / execute) sit outside all of them — including `POST /trigger/:name` for a flow literally NAMED `toggle`, which the toggle arm deliberately excludes so a name cannot cost a member its run door', proof: 'showcase-anonymous-deny-surfaces.dogfood.test.ts', // [2026-08-31] Ledger granularity for the same DOMAIN-WIDE gate named // above — the property the note already relies on ("gating the DOMAIN // rather than each route is what keeps a newly added automation route from // arriving ungated"). covers: ['automation:domains/automation.ts:anonymous-gate', 'dispatcher-domain:route-ledger.ts:/automation'], - note: 'Ungated, an anonymous caller could start real flow runs (`POST /:name/trigger`), read the full flow inventory (`GET /automation`), and DEREGISTER a registered flow (`DELETE /:name` → `{deleted:true}`) — the destructive one, which #5519 did not originally record. Gating the DOMAIN rather than each route is what keeps a newly added automation route from arriving ungated. Engine-internal triggers (record-change, schedule) never speak HTTP and are untouched.' }, + note: 'Ungated, an anonymous caller could start real flow runs (`POST /:name/trigger`), read the full flow inventory (`GET /automation`), and DEREGISTER a registered flow (`DELETE /:name` → `{deleted:true}`) — the destructive one, which the original anonymous-surface report did not record. Gating the DOMAIN rather than each route is what keeps a newly added automation route from arriving ungated. Engine-internal triggers (record-change, schedule) never speak HTTP and are untouched.' }, // #7033 / #7023 — the SIXTH dispatcher domain to join the baseline. `/packages` // was the last routed domain with ZERO authorization predicates: a survey drove // a guest-principal caller to a 200 on the DESTRUCTIVE `discard-drafts` and the @@ -260,7 +260,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // catch-all, and the legacy `HttpDispatcher.handlePackages` method) converges on // ONE handler body, `handlePackagesRequest`, so a single domain-wide gate there // covers them all. - { id: 'anonymous-deny-packages', summary: 'anonymous-deny on the package-management surface (#7033 / #7023)', state: 'enforced', + { id: 'anonymous-deny-packages', summary: 'anonymous-deny on the package-management surface', state: 'enforced', enforcement: 'runtime/domains/packages.ts handlePackagesRequest — shouldDenyAnonymous DOMAIN-WIDE as the handler\'s FIRST statement, ahead of the ObjectQL registry probe so the 401-vs-503 difference cannot fingerprint whether the package service is mounted; per-route capability predicates run after this floor — `manage_metadata` for every state-changing route (install / enable / disable / publish / publish-drafts / discard-drafts / commit-revert / rollback / revert / adopt-orphans / duplicate / manifest-PATCH / DELETE), and the ADR-0106 D4 read set (`studio.access` / `setup.access`) for every read (list / detail / commits / export)', proof: 'showcase-anonymous-deny-surfaces.dogfood.test.ts', // [2026-08-31] The DISPATCHER domain only. ⛔ NOT the REST `packages` @@ -297,9 +297,9 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // absence-recording row below included — so the identity story was one // `covers` append away from being optional. The admission rule in the // companion test now requires the classifying row to be `enforced`. - { id: 'realtime-delivery-authz', summary: 'realtime delivery fan-out has NO per-recipient authorization — trusted server-internal subscribers only (#2992 surface 2)', state: 'experimental', + { id: 'realtime-delivery-authz', summary: 'realtime delivery fan-out has NO per-recipient authorization — trusted server-internal subscribers only (a latent surface: identity admission is owed before any client transport ships)', state: 'experimental', covers: ['realtime:in-memory-realtime-adapter.ts:publish(trusted-fan-out)'], - note: 'Surface posture: system (trusted-implicit), pre-wiring — no end-user transport exists (handleUpgrade unimplemented, no REST subscribe route, client RealtimeAPI is a placeholder); the only subscribers are server-internal plugins (webhook auto-enqueuer, knowledge sync). Structural defect: Subscription carries no principal, matchesSubscription filters only by object+eventTypes (RealtimeSubscriptionOptions.filter is declared but never read), and the engine publishes the FULL after-row — so any future external subscriber would receive record bodies cross-tenant that its own find would hide. ADMISSION REQUIREMENT before any WebSocket/SSE/subscribe transport ships: per-recipient RLS/FLS/tenant re-check on delivery (subscription carries the subscriber ExecutionContext) OR id-only payload + client re-fetch. The transport tripwire probes in authz-conformance.test.ts turn a wired transport into an UNCLASSIFIED surface → red CI. [#9083] Clearing that red by classifying the new key HERE is refused: checkTransportWiredAdmission rejects a TRANSPORT-WIRED key covered by any row that is not `enforced`, and checkLedger separately requires an `enforced` row to name an enforcement site — the two compose into the admission requirement above. Before #9083 this note promised a gate that did not exist: appending the tripwire key to this row was measured green with ZERO authorization written (and the `removed` state admitted the identical exit), which is the quiet one-line exit that reads as compliance. The honest path out of the red is to write the per-recipient re-check FIRST, then classify the wired key on an `enforced` row naming that site — either this row upgraded (at which point its summary stops recording an absence and must be rewritten) or a new row beside it, leaving this one to carry the pre-wiring fan-out posture and its `publish(trusted-fan-out)` pin. Both are admissible; what the rule forbids is classifying the key while the classifying row still says no authorization exists.' }, + note: 'Surface posture: system (trusted-implicit), pre-wiring — no end-user transport exists (handleUpgrade unimplemented, no REST subscribe route, client RealtimeAPI is a placeholder); the only subscribers are server-internal plugins (webhook auto-enqueuer, knowledge sync). Structural defect: Subscription carries no principal, matchesSubscription filters only by object+eventTypes (RealtimeSubscriptionOptions.filter is declared but never read), and the engine publishes the FULL after-row — so any future external subscriber would receive record bodies cross-tenant that its own find would hide. ADMISSION REQUIREMENT before any WebSocket/SSE/subscribe transport ships: per-recipient RLS/FLS/tenant re-check on delivery (subscription carries the subscriber ExecutionContext) OR id-only payload + client re-fetch. The transport tripwire probes in authz-conformance.test.ts turn a wired transport into an UNCLASSIFIED surface → red CI. Clearing that red by classifying the new key HERE is refused: checkTransportWiredAdmission rejects a TRANSPORT-WIRED key covered by any row that is not `enforced`, and checkLedger separately requires an `enforced` row to name an enforcement site — the two compose into the admission requirement above. Before that admission rule landed, this note promised a gate that did not exist: appending the tripwire key to this row was measured green with ZERO authorization written (and the `removed` state admitted the identical exit), which is the quiet one-line exit that reads as compliance. The honest path out of the red is to write the per-recipient re-check FIRST, then classify the wired key on an `enforced` row naming that site — either this row upgraded (at which point its summary stops recording an absence and must be rewritten) or a new row beside it, leaving this one to carry the pre-wiring fan-out posture and its `publish(trusted-fan-out)` pin. Both are admissible; what the rule forbids is classifying the key while the classifying row still says no authorization exists.' }, // ── ADR-0096 — MCP execution-surface identity admission (#3167). The MCP // server exposes ObjectStack tool execution over two transports with DIFFERENT @@ -307,7 +307,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // either. (Corrects #3167's premise that the HTTP admission was missing — it // is wired; the real gap is the opt-in stdio transport.) { id: 'mcp-http-identity', summary: 'MCP HTTP surface (/api/v1/mcp) admits the caller identity — anonymous denied, OAuth scope-gated, caller ExecutionContext threaded to every tool\'s data op', state: 'enforced', - enforcement: 'runtime/http-dispatcher.ts handleMcp — requires ec.userId||ec.isSystem (401 else, RFC 9728 WWW-Authenticate advertised when the OAuth track is live); OAuth-token provenance narrows the exposed tool families to the granted MCP scopes (403 on none, #2698); buildMcpBridge(context) threads the caller ExecutionContext into every bridge op (callData(..., ec)), and mcp-server-runtime.ts handleHttpRequest builds a fresh per-request McpServer from that principal-bound bridge (registerObjectTools/registerActionTools) — so RLS / FLS / tenant apply exactly as on REST /data', + enforcement: 'runtime/http-dispatcher.ts handleMcp — requires ec.userId||ec.isSystem (401 else, RFC 9728 WWW-Authenticate advertised when the OAuth track is live); OAuth-token provenance narrows the exposed tool families to the granted MCP scopes (403 on none); buildMcpBridge(context) threads the caller ExecutionContext into every bridge op (callData(..., ec)), and mcp-server-runtime.ts handleHttpRequest builds a fresh per-request McpServer from that principal-bound bridge (registerObjectTools/registerActionTools) — so RLS / FLS / tenant apply exactly as on REST /data', // [2026-08-31] The dispatcher `/mcp` domain, at ledger granularity — the // same handler the two keys beside it already pin. ⛔ NOT `/mcp/skill`: // that is a second handler body (handleMcpSkillRequest) this row's @@ -317,7 +317,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ 'dispatcher-domain:route-ledger.ts:/mcp', ], proof: 'showcase-mcp-http-identity.dogfood.test.ts', - note: 'The per-request principal-bound tool server is isolated from the long-lived UNSCOPED stdio server (see mcp-stdio-authority). HIGH-RISK, proven end-to-end (#3167 PR-B): the proof boots the real showcase + security + MCP plugin and drives POST /api/v1/mcp — an anonymous tools/call is 401 before any tool runs, and a member\'s query_records over the owner-private showcase_private_note returns ONLY their own rows (if the tool ran unscoped/system — the stdio posture — the other owner\'s rows would leak). Dropping the buildMcpBridge(context) threading (or building an unscoped/system bridge for HTTP) makes the context-threaded key STALE → red CI; a new sibling MCP data handler appears as an UNCLASSIFIED surface until a row covers it. Dispatcher-level unit coverage: http-dispatcher.mcp.test.ts (401, EC-to-bridge) + http-dispatcher.mcp-oauth.test.ts (scope 403).' }, + note: 'The per-request principal-bound tool server is isolated from the long-lived UNSCOPED stdio server (see mcp-stdio-authority). HIGH-RISK, proven end-to-end: the proof boots the real showcase + security + MCP plugin and drives POST /api/v1/mcp — an anonymous tools/call is 401 before any tool runs, and a member\'s query_records over the owner-private showcase_private_note returns ONLY their own rows (if the tool ran unscoped/system — the stdio posture — the other owner\'s rows would leak). Dropping the buildMcpBridge(context) threading (or building an unscoped/system bridge for HTTP) makes the context-threaded key STALE → red CI; a new sibling MCP data handler appears as an UNCLASSIFIED surface until a row covers it. Dispatcher-level unit coverage: http-dispatcher.mcp.test.ts (401, EC-to-bridge) + http-dispatcher.mcp-oauth.test.ts (scope 403).' }, { id: 'mcp-stdio-authority', summary: 'MCP stdio transport admits an env-supplied API-key principal — RLS/FLS/tenant applied to record reads, fail-closed on a missing/invalid key, no `system` bypass (opt-in: autoStart / OS_MCP_STDIO_ENABLED=true + OS_MCP_STDIO_API_KEY)', state: 'enforced', enforcement: 'mcp/plugin.ts start() — when stdio auto-start is requested it resolves OS_MCP_STDIO_API_KEY through the SAME @objectstack/core chain as HTTP/REST (resolveStdioExecutionContext → resolveAuthzContext → resolveApiKeyPrincipal), builds the caller ExecutionContext, and threads it (re-resolved per call) into the record-reader passed to mcp-server-runtime.ts bridgeResources; record_by_id reads via ql.find(obj, { where:{id}, context }) so RLS/FLS/tenant apply exactly as on REST /data. FAIL-CLOSED: no key / no objectql / an unknown|revoked|expired|owner-less key throws and refuses to start stdio — there is no unscoped fallback and deliberately no OS_MCP_STDIO_IDENTITY=system bypass (full authority = a minted admin/service key; see ADR-0101).', covers: ['mcp:plugin.ts:stdio-principal-bound'], @@ -329,15 +329,15 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // ADR-0086 refusal, not the mere presence of `managedBy`. ⛔ The id stays in this // comment and out of the strings below — `check:doc-authoring` is right that a // runtime string reaches a reader with no tracker. - { id: 'readonly-static-write', summary: 'static `readonly: true` stripped from non-system UPDATE (#2948 / #3003) AND INSERT (#3043 at the ingress; in-engine for every caller since the 2026-09-03 ruling) payloads — neither a direct PATCH nor a direct POST can forge approval/status/amount columns the UI never renders', state: 'enforced', - enforcement: 'UPDATE: objectql/engine.ts stripReadonlyFields on the single-id + multi-row paths (#2948, caller-supplied VALUES only — the entry snapshot carries the caller payload, so a server stamp survives whether the hook ADDED the key or OVERWROTE one the caller also sent, #5591). INSERT: objectql/engine.ts runs the SAME stripReadonlyFields inside engine.insert (maintainer ruling 2026-09-03, option C — one semantics, one enforcement point; the metadata-protocol ingress copy that used to cover only the DataProtocol faces while a direct engine.insert caller bypassed it is deleted), after the beforeInsert hooks and before validation, over staticReadonlyInsertSubject (runtime-owned types keep their own pass; the sys_ namespace and the platform-internal managedBy buckets — engine-owned / append-only / better-auth — their own 403 guards — that exclusion was narrowed from managedBy set to anything, so a user-writable bucket is judged on create exactly as on update); the field re-derives its defaultValue. isSystem exempt on both. An internal writer that seeds a readonly column on create does so by one of exactly two mechanisms: (1) a system context — identity provisioning (plugin-auth objectql-adapter.ts wraps its engine in withSystemContext); (2) the platform-object carve-out in staticReadonlyInsertSubject (rule-validator.ts) — a sys_-prefixed object, or one in a platform-internal managedBy bucket, is outside the strip subject (a user-writable bucket is NOT), so the metadata repository (sys-metadata-repository.ts) seeds sys_metadata_history provenance (recorded_by, readonly lookup) and its event-log cursor (event_seq, readonly number) under the CALLER context, and those columns are policed by the object guards of that platform object rather than swallowed by the strip — as is every other platform-object seeder; symmetric with the readonlyWhen strip', + { id: 'readonly-static-write', summary: 'static `readonly: true` stripped from non-system UPDATE AND INSERT (first at the data-write ingress; in-engine for every caller since the 2026-09-03 ruling) payloads — neither a direct PATCH nor a direct POST can forge approval/status/amount columns the UI never renders', state: 'enforced', + enforcement: 'UPDATE: objectql/engine.ts stripReadonlyFields on the single-id + multi-row paths (caller-supplied VALUES only — the entry snapshot carries the caller payload, so a server stamp survives whether the hook ADDED the key or OVERWROTE one the caller also sent). INSERT: objectql/engine.ts runs the SAME stripReadonlyFields inside engine.insert (maintainer ruling 2026-09-03, option C — one semantics, one enforcement point; the metadata-protocol ingress copy that used to cover only the DataProtocol faces while a direct engine.insert caller bypassed it is deleted), after the beforeInsert hooks and before validation, over staticReadonlyInsertSubject (runtime-owned types keep their own pass; the sys_ namespace and the platform-internal managedBy buckets — engine-owned / append-only / better-auth — their own 403 guards — that exclusion was narrowed from managedBy set to anything, so a user-writable bucket is judged on create exactly as on update); the field re-derives its defaultValue. isSystem exempt on both. An internal writer that seeds a readonly column on create does so by one of exactly two mechanisms: (1) a system context — identity provisioning (plugin-auth objectql-adapter.ts wraps its engine in withSystemContext); (2) the platform-object carve-out in staticReadonlyInsertSubject (rule-validator.ts) — a sys_-prefixed object, or one in a platform-internal managedBy bucket, is outside the strip subject (a user-writable bucket is NOT), so the metadata repository (sys-metadata-repository.ts) seeds sys_metadata_history provenance (recorded_by, readonly lookup) and its event-log cursor (event_seq, readonly number) under the CALLER context, and those columns are policed by the object guards of that platform object rather than swallowed by the strip — as is every other platform-object seeder; symmetric with the readonlyWhen strip', proof: 'showcase-static-readonly.dogfood.test.ts', - note: 'The #3003 field report: `readonly: true` used to be UI-only, so a logged-in non-admin self-approved a 4-stage approval (approval_status/approval_stage/confirmed_total) with one same-session REST PATCH on a draft record — RECORD_LOCKED only guards pending flows, and the draft never entered one. #3043 is the INSERT face: the same non-admin could skip the draft entirely and POST a record already `approval_status:"approved"` — a step SHORTER than #3003, and one the UPDATE strip never reached. It was first enforced at the DATA-WRITE INGRESS (not the engine) so it covered every external caller — REST, the GraphQL/MCP dispatcher, bulk import — without stripping internal writers; the maintainer ruling of 2026-09-03 (option C) moved it INTO the engine, so a direct engine.insert caller is covered too. A writer that seeds a readonly column on create does so under a system context (identity provisioning) or, for a platform-internal object (sys_-prefixed, or a managedBy bucket that fails closed on a user write), under the carve-out in staticReadonlyInsertSubject that leaves such objects to their own guards — the metadata repository writing sys_metadata_history.recorded_by provenance and its event_seq event-log cursor under the caller context is that second mechanism, not a system context. The strip is SILENT on both paths (HTTP 2xx, forged value dropped; a stripped INSERT field falls back to its defaultValue). `readonlyWhen` stays INSERT-exempt (a conditional lock needs a prior record). System-context writes (import, seed replay, migration) still seed readonly columns. Engine proof in objectql engine-insert-static-readonly-strip.test.ts (forge stripped against a real ObjectQL, default re-seeded, system context allowed, hook stamps survive, strict refuses); delegation proof in metadata-protocol protocol.readonly-insert.test.ts (every create face forwards whole and surfaces the engine verdict as droppedFields where its contract carries one).' }, + note: 'The originating field report: `readonly: true` used to be UI-only, so a logged-in non-admin self-approved a 4-stage approval (approval_status/approval_stage/confirmed_total) with one same-session REST PATCH on a draft record — RECORD_LOCKED only guards pending flows, and the draft never entered one. The INSERT face followed: the same non-admin could skip the draft entirely and POST a record already `approval_status:"approved"` — a step SHORTER than the draft-then-PATCH route, and one the UPDATE strip never reached. It was first enforced at the DATA-WRITE INGRESS (not the engine) so it covered every external caller — REST, the GraphQL/MCP dispatcher, bulk import — without stripping internal writers; the maintainer ruling of 2026-09-03 (option C) moved it INTO the engine, so a direct engine.insert caller is covered too. A writer that seeds a readonly column on create does so under a system context (identity provisioning) or, for a platform-internal object (sys_-prefixed, or a managedBy bucket that fails closed on a user write), under the carve-out in staticReadonlyInsertSubject that leaves such objects to their own guards — the metadata repository writing sys_metadata_history.recorded_by provenance and its event_seq event-log cursor under the caller context is that second mechanism, not a system context. The strip is SILENT on both paths (HTTP 2xx, forged value dropped; a stripped INSERT field falls back to its defaultValue). `readonlyWhen` stays INSERT-exempt (a conditional lock needs a prior record). System-context writes (import, seed replay, migration) still seed readonly columns. Engine proof in objectql engine-insert-static-readonly-strip.test.ts (forge stripped against a real ObjectQL, default re-seeded, system context allowed, hook stamps survive, strict refuses); delegation proof in metadata-protocol protocol.readonly-insert.test.ts (every create face forwards whole and surfaces the engine verdict as droppedFields where its contract carries one).' }, // ── ADR-0057 — ERP authorization core (enforced + e2e proven) ────────── { id: 'scope-depth', summary: 'permission-grant access DEPTH (own/own_and_reports/unit/unit_and_below/org)', state: 'enforced', enforcement: 'plugin-security getEffectiveScope (stash) + plugin-sharing delegates HIERARCHY scopes to a pluggable IHierarchyScopeResolver (open: fail-closed to own; enterprise @objectstack/security-enterprise; reference resolver in this proof) — ADR-0057 D1', proof: 'showcase-scope-depth.dogfood.test.ts' }, - { id: 'declarative-rbac-seeding', summary: 'stack-declared roles + sharingRules seeded at boot (#2077)', state: 'enforced', + { id: 'declarative-rbac-seeding', summary: 'stack-declared roles + sharingRules seeded at boot', state: 'enforced', enforcement: 'plugin-security bootstrapDeclaredPositions + plugin-sharing bootstrapDeclaredSharingRules — ADR-0057 D6', proof: 'showcase-declarative-rbac-seeding.dogfood.test.ts' }, { id: 'declarative-permission-seeding', summary: 'stack-declared permission sets seeded into sys_permission_set with package provenance (packageId + managed_by)', state: 'enforced', enforcement: 'plugin-security bootstrapDeclaredPermissions — ADR-0086 D5 (managed_by:package re-seeded on boot/upgrade; env-authored platform/user/legacy rows never clobbered); provenance fields ADR-0086 D3 (spec PermissionSetSchema.packageId/managedBy + sys_permission_set.package_id/managed_by)', proof: 'showcase-permission-seeding.dogfood.test.ts', @@ -352,14 +352,14 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ enforcement: 'plugin-security/field-masker.ts + detectForbiddenWrites' }, { id: 'ownership-stamp', summary: 'owner_id auto-stamp on insert', state: 'enforced', enforcement: 'plugin-security/security-plugin.ts (insert owner_id inject)' }, - { id: 'ownership-anchor-guard', summary: 'owner_id is system-managed for non-privileged writers — no client forge (insert) / transfer (update) without the transfer grant (#3004)', state: 'enforced', + { id: 'ownership-anchor-guard', summary: 'owner_id is system-managed for non-privileged writers — no client forge (insert) / transfer (update) without the transfer grant', state: 'enforced', enforcement: 'plugin-security/security-plugin.ts step 3.5: insert forging a foreign owner is denied unless allowTransfer/modifyAllRecords (batch rows too); update carrying owner_id is a transfer/disown, denied without the grant — single-id no-op echo tolerated via pre-image compare, bulk change-set fails closed; isSystem exempt', proof: 'owner-anchor-and-bulk-writes.dogfood.test.ts' }, - { id: 'bulk-write-owner-scoping', summary: 'bulk (multi) update/delete are owner-scoped on OWD-private objects, not just single-id writes (#2982)', state: 'enforced', + { id: 'bulk-write-owner-scoping', summary: 'bulk (multi) update/delete are owner-scoped on OWD-private objects, not just single-id writes', state: 'enforced', enforcement: 'objectql/engine.ts seeds opCtx.ast for no-single-id update/delete BEFORE the middleware chain and hands the composed AST to driver.updateMany/deleteMany, so plugin-sharing buildWriteFilter (owner-match + shares) and plugin-security RLS write filters actually bind bulk writes', proof: 'owner-anchor-and-bulk-writes.dogfood.test.ts' }, - { id: 'public-form-managed-anchors', summary: 'anonymous public-form submit cannot supply server-managed anchors (owner_id / organization_id / audit / id) — #3022', state: 'enforced', - enforcement: 'spec/security/public-form.ts PUBLIC_FORM_SERVER_MANAGED_FIELDS shared by rest/rest-server.ts form routes (allow-list + schema/section/lookup exposure) AND plugin-security publicFormGrant branch (strips every insert row before the grant admits the write — the data-layer boundary the grant otherwise bypasses; complements the #3004 step 3.5 guard, which the grant short-circuits)', + { id: 'public-form-managed-anchors', summary: 'anonymous public-form submit cannot supply server-managed anchors (owner_id / organization_id / audit / id)', state: 'enforced', + enforcement: 'spec/security/public-form.ts PUBLIC_FORM_SERVER_MANAGED_FIELDS shared by rest/rest-server.ts form routes (allow-list + schema/section/lookup exposure) AND plugin-security publicFormGrant branch (strips every insert row before the grant admits the write — the data-layer boundary the grant otherwise bypasses; complements the step 3.5 owner-anchor guard, which the grant short-circuits)', proof: 'showcase-public-form.dogfood.test.ts', note: 'Proof file carries the forged owner_id/organization_id submit case; the route-level matrix is covered unit-side in rest public-form-routes.test.ts + plugin-security security-plugin.test.ts (publicFormGrant strip suite).' }, { id: 'record-share', summary: 'manual record shares (sys_record_share)', state: 'enforced', @@ -375,19 +375,19 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ proof: 'showcase-bu-hierarchy-sharing.dogfood.test.ts', note: 'Recipient list re-synced when the `field` kind became enforced. ⭐ These summaries are EXHAUSTIVE, not indicative — recorded here so the gap is not re-filed a third time. The parenthetical list is an order-faithful transcription of the AUTHORABLE enum `ShareRecipientType` (spec/security/sharing.zod.ts), and the file backs that reading: of the 49 `summary` strings, 0 hedge (none carries an `e.g.`, a `such as`, an `etc` or an ellipsis), and the closest structural twin — `scope-depth` — transcribes `ObjectAccessScopeSchema` member-for-member in declaration order. So the drift was a defect, not a stylistic gap, and declaring these summaries INDICATIVE was considered and REFUSED: it would retro-weaken the 16 rows that enumerate, to buy one row a caveat it does not need. `queue` is the one runtime `SharingRuleRecipientType` member deliberately absent: it is reserved (no `sys_queue`), expands to [] and is NOT authorable, so naming it on an `enforced` row would make this row false in the OTHER direction. ⚠️ Nothing mechanical reads `summary` / `enforcement` / `note` — this row is hand-maintained, so re-check it against `ShareRecipientType` whenever that enum moves.' }, { id: 'hierarchy-widening', summary: 'hierarchy widening — a unit + its subordinate units gain access', state: 'enforced', - enforcement: 'plugin-sharing/business-unit-graph.ts BusinessUnitGraphService.expandUsers subtree (unit_and_subordinates recipient) — ADR-0057 D5 re-homed off the never-existent sys_position.parent. The narrower business_unit recipient resolves through expandUnitMembers (exactly one unit, no descent) and is pinned by the same proof file: #7807 narrowed the runtime to the declaration after both kinds shared one subtree walk', + enforcement: 'plugin-sharing/business-unit-graph.ts BusinessUnitGraphService.expandUsers subtree (unit_and_subordinates recipient) — ADR-0057 D5 re-homed off the never-existent sys_position.parent. The narrower business_unit recipient resolves through expandUnitMembers (exactly one unit, no descent) and is pinned by the same proof file: the runtime was narrowed to the declaration after both kinds shared one subtree walk', proof: 'showcase-bu-hierarchy-sharing.dogfood.test.ts' }, { id: 'rls-compiler-fail-closed', summary: 'uncompilable RLS predicate is surfaced/denied, not dropped', state: 'enforced', - enforcement: 'plugin-security/rls-compiler.ts compileFilter (drop + warn + RLS_DENY_FILTER) on the shape gate formula/rls-predicate.ts isSupportedRlsExpression — hoisted out of plugin-security in #4983 so lint/validate-rls-predicate-enforceability.ts can REJECT the same predicate at authoring time (ADR-0056 D4), from the one definition' }, + enforcement: 'plugin-security/rls-compiler.ts compileFilter (drop + warn + RLS_DENY_FILTER) on the shape gate formula/rls-predicate.ts isSupportedRlsExpression — hoisted out of plugin-security so lint/validate-rls-predicate-enforceability.ts can REJECT the same predicate at authoring time (ADR-0056 D4), from the one definition' }, { id: 'system-permissions', summary: 'systemPermissions / tab-app gating', state: 'enforced', enforcement: 'rest/rest-server.ts filterAppForUser' }, { id: 'secure-by-default-posture', summary: 'ADR-0066 ④ — sensitive system objects opt out of the wildcard grant (access.default: private)', state: 'enforced', - enforcement: 'plugin-security/permission-evaluator.ts resolveObjectPermission (plain wildcard does not cover a private object) + posture-gated superuser bypass; declarations in platform-objects (sys_secret, sys_jwks, sys_verification, sys_oauth_access_token, sys_oauth_refresh_token, sys_device_code) + sys_sso_provider D3 capability gate (the gate\'s other carrier, sys_scim_provider, retired under #11757)', + enforcement: 'plugin-security/permission-evaluator.ts resolveObjectPermission (plain wildcard does not cover a private object) + posture-gated superuser bypass; declarations in platform-objects (sys_secret, sys_jwks, sys_verification, sys_oauth_access_token, sys_oauth_refresh_token, sys_device_code) + sys_sso_provider D3 capability gate (the gate\'s other carrier, sys_scim_provider, retired once the stable SCIM line stopped deriving a provider model)', note: 'Primitive enforcement unit-proven in plugin-security/security-plugin.test.ts (ADR-0066 posture suite); the per-object declarations are pinned by platform-objects.test.ts "secure-by-default posture" so dropping the flag from a secret store fails CI, not review. Member self-service objects (sys_session, sys_api_key, sys_oauth_application, sys_two_factor) deliberately stay public-posture — the Account app reads them with a member context; row scoping (owner/tenant RLS + _self carve-outs) is their guard.' }, - { id: 'flow-run-as', summary: 'flow runAs — data nodes execute under the run\'s effective identity (#1888)', state: 'enforced', + { id: 'flow-run-as', summary: 'flow runAs — data nodes execute under the run\'s effective identity', state: 'enforced', enforcement: 'service-automation/engine.ts runAs authorization envelope → runtime-identity.ts → builtin/crud-nodes.ts (runAs:\'system\' → RLS-bypassing; runAs:\'user\' → trigger identity, RLS enforced as that user)', proof: 'flow-runas.dogfood.test.ts', - note: 'ADR-0049 originally classified runAs as roadmap M2, but #1888 implemented it for flow data nodes (create/update/delete/query run under the chosen identity). Also proven for scheduled flows in flow-runas-schedule.dogfood.test.ts.' }, + note: 'ADR-0049 originally classified runAs as roadmap M2, but its enforce-or-remove decision chose ENFORCE and implemented it for flow data nodes (create/update/delete/query run under the chosen identity). Also proven for scheduled flows in flow-runas-schedule.dogfood.test.ts.' }, // ── ADR-0049 / #8613 — the RBAC grant catalogues' `active` switch ────── // @@ -417,10 +417,10 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // predicates like this one — widening was measured unachievable in general // form. See this file's header for the narrowed claim and the measured // numbers. - { id: 'permission-set-active', summary: '`sys_permission_set.active` — a deactivated permission set grants nothing (ADR-0049 / #8613)', state: 'enforced', + { id: 'permission-set-active', summary: '`sys_permission_set.active` — a deactivated permission set grants nothing (ADR-0049)', state: 'enforced', enforcement: 'core/security/resolve-authz-context.ts step 6b — isRowActive drops the row BEFORE any derivation, so a deactivated set contributes no name to `grants.permissions`, no systemPermissions and no tabPermissions, AND `hasPlatformAdminGrant` cannot be read off a deactivated `admin_full_access`; plugin-security/security-plugin.ts dbLoader applies the SAME predicate, which is the only place a set reached by NAME is judged (position names are commonly reused as set names, so an ACTIVE position carries a DEACTIVATED set\'s name that far); plugin-auth/last-admin-guard.ts carries `active` in PERMISSION_SET_STANDING_KEYS so deactivating the last admin set is judged as an emptying rather than read as a bootstrap window', - note: 'Unit-proven; an e2e dogfood proof is a follow-on, the same disposition as the ADR-0105 block above and for the same reason — the flag is a predicate inside the grant resolver, not an HTTP surface, so there is no route for a dogfood boot to drive at it directly. core/security/resolve-authz-context.test.ts "[#8613] the `active` flag on the grant catalogues (ADR-0049)" covers the derivation half, including THE HIGH-BLAST-RADIUS CASE (a deactivated admin_full_access confers no PLATFORM_ADMIN) and that deactivating ONE set leaves the others granting; plugin-security/permission-set-active.test.ts covers the loader half, including THE REACHABILITY CASE (a position name reaching a deactivated set of the same name) plus its non-vacuous twin (the same request with the set ACTIVE does resolve); core/security/row-active.test.ts pins the predicate itself (absent grants, junk does not revoke, 0/1 deactivates). Deliberately NOT in HIGH_RISK: that list marks primitives guarding object data through a sibling HTTP entry point, and this one guards grant DERIVATION. The honest upgrade path is a real proof (seed a deactivated set, drive REST as its holder, observe the refusal), not re-citing a neighbouring file.' }, - { id: 'position-active', summary: '`sys_position.active` — a deactivated position grants nothing, and its name cannot resolve the grant one layer down (ADR-0049 / #8613)', state: 'enforced', + note: 'Unit-proven; an e2e dogfood proof is a follow-on, the same disposition as the ADR-0105 block above and for the same reason — the flag is a predicate inside the grant resolver, not an HTTP surface, so there is no route for a dogfood boot to drive at it directly. core/security/resolve-authz-context.test.ts "the `active` flag on the grant catalogues (ADR-0049)" covers the derivation half, including THE HIGH-BLAST-RADIUS CASE (a deactivated admin_full_access confers no PLATFORM_ADMIN) and that deactivating ONE set leaves the others granting; plugin-security/permission-set-active.test.ts covers the loader half, including THE REACHABILITY CASE (a position name reaching a deactivated set of the same name) plus its non-vacuous twin (the same request with the set ACTIVE does resolve); core/security/row-active.test.ts pins the predicate itself (absent grants, junk does not revoke, 0/1 deactivates). Deliberately NOT in HIGH_RISK: that list marks primitives guarding object data through a sibling HTTP entry point, and this one guards grant DERIVATION. The honest upgrade path is a real proof (seed a deactivated set, drive REST as its holder, observe the refusal), not re-citing a neighbouring file.' }, + { id: 'position-active', summary: '`sys_position.active` — a deactivated position grants nothing, and its name cannot resolve the grant one layer down (ADR-0049)', state: 'enforced', enforcement: 'core/security/resolve-authz-context.ts step 6a — isRowActive gates BOTH halves, and only both hold it: (i) only ACTIVE position ids collect their `sys_position_permission_set` linkage, so a deactivated position carries no bound set; (ii) the deactivated NAME is dropped from `grants.positions`, because resolvePermissionSetsForContext requests positions as permission-set NAMES and a name left standing resolves the same grant one layer down', note: 'Only a name whose `sys_position` row is EXPLICITLY deactivated is dropped — a name with no row at all (`org_owner`, a membership-derived role, the built-in `everyone` audience anchor) has no flag to read and is untouched. Deliberately NOT a blanket revocation of the sets themselves: a set held via BOTH a deactivated position AND a direct user grant still resolves, since the direct grant is a different grant (resolve-authz-context.test.ts pins exactly that case). Symmetrically, the WRITE gates and blast-radius reads in plugin-security (assertAudienceAnchorBindingGate, setsBoundToPosition, the delegated-admin surfaces) stay UNFILTERED on purpose — dropping a deactivated row there would make a refused binding permitted, narrow a delegate\'s boundary, and make a deactivated position unmanageable. Unit-proven in core/security/resolve-authz-context.test.ts (a deactivated position stops granting its sets; an active one still grants; an absent column grants; the 0/1 shape deactivates; deactivating ONE position leaves the others granting) + core/security/row-active.test.ts. Not HIGH_RISK for the same reason as `permission-set-active`.' }, @@ -440,17 +440,17 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // unlike API-key `isExpired`, because a grant row is standing authority // rather than a single credential. { id: 'grant-validity-window', summary: '`valid_from` / `valid_until` on a grant row — a grant outside its half-open window resolves to nothing, at resolution time and with no cleanup job (ADR-0091 D1/D2)', state: 'enforced', - enforcement: 'core/security/grant-validity.ts isGrantActive — ONE half-open [valid_from, valid_until) predicate (UTC; absent bound = unbounded; a PRESENT but unparseable bound fails CLOSED), applied by every reader that turns a window-carrying row into authority: core/security/resolve-authz-context.ts step 3 (sys_member — BOTH derivations off the one read: accessible_org_ids and, since #10982, the org-administration role projection), step 4 (sys_user_position) and step 6 (sys_user_permission_set, dropped BEFORE any derivation so an expired admin_full_access cannot yield platform_admin either); plugin-security/explain-engine.ts buildContextForUser (plus isGrantExpired for the dedicated "expired" contributor state); plugin-sharing/position-graph.ts PositionGraphService.expandPositionUsers; plugin-security/delegated-admin-gate.ts held-scope resolution; plugin-auth/last-admin-guard.ts administrator-standing reads; plugin-approvals/approval-service.ts lookupActiveDelegation (sys_approval_delegation); runtime/domains/keys.ts membership check', + enforcement: 'core/security/grant-validity.ts isGrantActive — ONE half-open [valid_from, valid_until) predicate (UTC; absent bound = unbounded; a PRESENT but unparseable bound fails CLOSED), applied by every reader that turns a window-carrying row into authority: core/security/resolve-authz-context.ts step 3 (sys_member — BOTH derivations off the one read: accessible_org_ids and the org-administration role projection), step 4 (sys_user_position) and step 6 (sys_user_permission_set, dropped BEFORE any derivation so an expired admin_full_access cannot yield platform_admin either); plugin-security/explain-engine.ts buildContextForUser (plus isGrantExpired for the dedicated "expired" contributor state); plugin-sharing/position-graph.ts PositionGraphService.expandPositionUsers; plugin-security/delegated-admin-gate.ts held-scope resolution; plugin-auth/last-admin-guard.ts administrator-standing reads; plugin-approvals/approval-service.ts lookupActiveDelegation (sys_approval_delegation); runtime/domains/keys.ts membership check', proof: 'delegation-of-duty.dogfood.test.ts', - note: '[#8811] NO `covers`, and — exactly like the two `active` rows above — that is a statement about the RATCHET rather than an omission: `discover()` enumerates HTTP entry points from a curated per-file probe table, and a predicate inside an existing resolver adds no entry point, so this primitive could never have surfaced as UNCLASSIFIED or STALE during any period it was inert. Per the #8711 ruling (2026-08-15) the invariant\'s advertised SCOPE is narrowed to what the ratchet can check; this row is owed under the hand-maintained half the file header still states. WHAT IS AND IS NOT WINDOW-FILTERED — measured at the call sites, not taken from the filing: the columns are declared on exactly THREE objects (sys_user_position, sys_user_permission_set, sys_approval_delegation), and on all three EVERY authorization-resolution seam applies the predicate, which is what makes this row `enforced` rather than partial. Two reads deliberately do NOT filter and neither is a live hole: (i) the resolver\'s FELLOW-ORG read (step 5, `sys_member {organization_id}`) projects peer user_ids into `org_user_ids` for identity-table RLS — that is a collaborator roster, not a grant, and it is a different read from the principal\'s own memberships; (ii) plugin-approvals\' own expandPositionUsers projects user_id alone by the #8710 ruling, because approval ROUTING is not grant resolution — that note explicitly forbids "fixing" it with a filter. [#10982] `sys_member` still carries no window columns, so every read of it stays FORWARD-LOOKING — but BOTH derivations off the principal\'s own membership read (step 3) now apply the predicate. Maintainer ruling, 2026-08-22 live session (item 2): a lapsed membership is NO MEMBERSHIP, not merely no org access, so the org-administration role projection is window-filtered BEFORE the derivation exactly as step 6 is. That closed a real defect IN the enforcement rather than in this ledger: the row\'s state was and remains `enforced`, and what changed is that the principal\'s own membership read is no longer one of the unfiltered ones listed above. [#11089] last-admin-guard.ts\'s MEMBER standing-key notes (`valid_from`/`valid_until` in STANDING_KEY_EXCLUSIONS) were re-verified against the merged ref and rewritten to describe the post-#10982 window-filtered projection; the exclusion decision itself was and is unaffected (that guard counts administrators by GRADE alone), and its note now names the move owed the day the columns land (the spellings shift into MEMBER_STANDING_KEYS and the member count adopts the same predicate). Predicate unit-pinned in core/security/grant-validity.test.ts (half-open boundaries, seconds-vs-ms epoch, fail-closed on garbage, camelCase spellings); the resolver halves in core/security/resolve-authz-context.test.ts "grant validity windows (ADR-0091 D1/D2)". Deliberately NOT in HIGH_RISK for the same reason as the `active` rows — that list marks primitives guarding object DATA through a sibling HTTP entry point, and this one guards grant DERIVATION. Unlike those rows an end-to-end proof EXISTS and is now cited (#9377): delegation-of-duty.dogfood.test.ts boots a real stack and asserts the delegate STOPS resolving the delegated sys_user_position at valid_until through the real resolveAuthzContext, and its header now claims this row back — the #7976 mutual-attribution contract is satisfied.' }, + note: 'NO `covers`, and — exactly like the two `active` rows above — that is a statement about the RATCHET rather than an omission: `discover()` enumerates HTTP entry points from a curated per-file probe table, and a predicate inside an existing resolver adds no entry point, so this primitive could never have surfaced as UNCLASSIFIED or STALE during any period it was inert. Per the 2026-08-15 maintainer ruling the invariant\'s advertised SCOPE is narrowed to what the ratchet can check; this row is owed under the hand-maintained half the file header still states. WHAT IS AND IS NOT WINDOW-FILTERED — measured at the call sites, not taken from the filing: the columns are declared on exactly THREE objects (sys_user_position, sys_user_permission_set, sys_approval_delegation), and on all three EVERY authorization-resolution seam applies the predicate, which is what makes this row `enforced` rather than partial. Two reads deliberately do NOT filter and neither is a live hole: (i) the resolver\'s FELLOW-ORG read (step 5, `sys_member {organization_id}`) projects peer user_ids into `org_user_ids` for identity-table RLS — that is a collaborator roster, not a grant, and it is a different read from the principal\'s own memberships; (ii) plugin-approvals\' own expandPositionUsers projects user_id alone by the 2026-08-15 ruling that access-conferring paths filter and addressing paths do not, because approval ROUTING is not grant resolution — that note explicitly forbids "fixing" it with a filter. `sys_member` still carries no window columns, so every read of it stays FORWARD-LOOKING — but BOTH derivations off the principal\'s own membership read (step 3) now apply the predicate. Maintainer ruling, 2026-08-22 live session (item 2): a lapsed membership is NO MEMBERSHIP, not merely no org access, so the org-administration role projection is window-filtered BEFORE the derivation exactly as step 6 is. That closed a real defect IN the enforcement rather than in this ledger: the row\'s state was and remains `enforced`, and what changed is that the principal\'s own membership read is no longer one of the unfiltered ones listed above. last-admin-guard.ts\'s MEMBER standing-key notes (`valid_from`/`valid_until` in STANDING_KEY_EXCLUSIONS) were re-verified against the merged ref and rewritten to describe the role projection as window-filtered now; the exclusion decision itself was and is unaffected (that guard counts administrators by GRADE alone), and its note now names the move owed the day the columns land (the spellings shift into MEMBER_STANDING_KEYS and the member count adopts the same predicate). Predicate unit-pinned in core/security/grant-validity.test.ts (half-open boundaries, seconds-vs-ms epoch, fail-closed on garbage, camelCase spellings); the resolver halves in core/security/resolve-authz-context.test.ts "grant validity windows (ADR-0091 D1/D2)". Deliberately NOT in HIGH_RISK for the same reason as the `active` rows — that list marks primitives guarding object DATA through a sibling HTTP entry point, and this one guards grant DERIVATION. Unlike those rows an end-to-end proof EXISTS and is now cited: delegation-of-duty.dogfood.test.ts boots a real stack and asserts the delegate STOPS resolving the delegated sys_user_position at valid_until through the real resolveAuthzContext, and its header now claims this row back — the mutual-attribution contract (a cited proof file names the rows it proves) is satisfied.' }, // ── Experimental — declared, NOT enforced (ADR-0049/0056 D8) ─────────── { id: 'field-encryption', summary: 'at-rest field encryption', state: 'experimental', note: 'no crypto provider reads the config; marked [EXPERIMENTAL] (D8). Deliberately KEPT (2026-07 D8 disposition): at-rest encryption is a real enterprise roadmap item with a stable schema shape — removing and re-adding would cost more (ADR-0087) than carrying it marked.' }, // ── Removed — by ADR-0056 D8 "design+enforce or remove" (2026-07) ────── - { id: 'agent-visibility', summary: 'AI agent `visibility` listing scope (#1901)', state: 'removed', - note: 'REMOVED from spec (agent.zod.ts `visibility` deleted, #1901). Never enforced — the chat-access evaluator excluded it and the agent list route did not filter by it, so `private` never hid an agent. Unlike field-encryption it has NO stable schema shape to preserve: correct enforcement needs owner/org anchors that do not exist (agents carry no owner field; the `EXTERNAL` posture rung is never derived), so the semantics — not just the plumbing — are undesigned. Per D8 a security-shaped field that lies is dropped, not carried marked. `access`/`permissions` ARE enforced at the chat route (#1884); re-introduce `visibility` when the listing surface gains real owner/org semantics.' }, + { id: 'agent-visibility', summary: 'AI agent `visibility` listing scope', state: 'removed', + note: 'REMOVED from spec (agent.zod.ts `visibility` deleted). Never enforced — the chat-access evaluator excluded it and the agent list route did not filter by it, so `private` never hid an agent. Unlike field-encryption it has NO stable schema shape to preserve: correct enforcement needs owner/org anchors that do not exist (agents carry no owner field; the `EXTERNAL` posture rung is never derived), so the semantics — not just the plumbing — are undesigned. Per D8 a security-shaped field that lies is dropped, not carried marked. `access`/`permissions` ARE enforced at the chat route; re-introduce `visibility` when the listing surface gains real owner/org semantics.' }, { id: 'compliance-configs', summary: 'GDPR/HIPAA/PCI configs', state: 'removed', note: 'REMOVED from spec (system/compliance.zod.ts deleted). Compliance-grade config must never merely look live: a parsed-but-dead `gdpr:` block is a liability in an audit. A real compliance subsystem will be designed top-down (data-subject rights engine, retention enforcer) when scheduled.' }, { id: 'data-masking', summary: 'role-based data masking', state: 'removed', @@ -459,9 +459,9 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ note: 'REMOVED from spec (rls.zod.ts — RLSConfigSchema/RLSAuditEventSchema/RLSAuditConfigSchema deleted). The enforced RLS path (plugin-security computeRlsFilter) never read them; per-policy RowLevelSecurityPolicySchema is the live surface and is unchanged.' }, { id: 'requireAuth-removed', summary: 'anonymous access to object data is denied unconditionally (no opt-out)', state: 'enforced', enforcement: 'core/security/anonymous-deny.ts shouldDenyAnonymous — no `requireAuth` input; every seam denies an anonymous, non-system caller outside the control-plane allowlist. spec tombstones `api.requireAuth` (retiredKey).', - note: 'ADR-0056 D2 → #3963: the `requireAuth: false` opt-out is RETIRED, not merely defaulted-on. Legitimate session-less surfaces survive by DECLARATION, not by posture: public-form submission (publicFormGrant), share-links (token → SYSTEM read), and public-book reads (audience:public, §6.7). A stack that mounts no auth now FAILS AT BOOT (cli/serve.ts, plugin-dev) instead of getting an explicit fail-open. [#7976] The `showcase-anonymous-deny.dogfood.test.ts` CITATION WAS DROPPED under mutual attribution — that file drives the platform default and observes 401, which is precisely what the `anonymous-deny` row (same file) already claims; it never authors `requireAuth: false`, never reads the spec tombstone and never boots an auth-less stack, so it cannot prove the distinguishing half of THIS row (that there is no opt-out). The retirement is pinned elsewhere and unit-side: the spec tombstone + the ADR-0087 conversion entry `stack.api.requireAuth` (conversions/registry.ts, which strips a surviving key) and rest/rest-auth-gate.test.ts. Not high-risk, so the row is sound without a dogfood proof; writing a real one (author `api: { requireAuth: false }` → expect the boot/authoring rejection) is the honest upgrade path, not re-citing the posture proof.' }, + note: 'ADR-0056 D2, completed by deleting the switch once every session-less surface was declared: the `requireAuth: false` opt-out is RETIRED, not merely defaulted-on. Legitimate session-less surfaces survive by DECLARATION, not by posture: public-form submission (publicFormGrant), share-links (token → SYSTEM read), and public-book reads (audience:public, §6.7). A stack that mounts no auth now FAILS AT BOOT (cli/serve.ts, plugin-dev) instead of getting an explicit fail-open. The `showcase-anonymous-deny.dogfood.test.ts` CITATION WAS DROPPED under mutual attribution — that file drives the platform default and observes 401, which is precisely what the `anonymous-deny` row (same file) already claims; it never authors `requireAuth: false`, never reads the spec tombstone and never boots an auth-less stack, so it cannot prove the distinguishing half of THIS row (that there is no opt-out). The retirement is pinned elsewhere and unit-side: the spec tombstone + the ADR-0087 conversion entry `stack.api.requireAuth` (conversions/registry.ts, which strips a surviving key) and rest/rest-auth-gate.test.ts. Not high-risk, so the row is sound without a dogfood proof; writing a real one (author `api: { requireAuth: false }` → expect the boot/authoring rejection) is the honest upgrade path, not re-citing the posture proof.' }, // ── Removed — by ADR-0049 (roadmap M2) ───────────────────────────────── { id: 'allow-transfer-restore-purge', summary: 'transfer/restore/purge ops (transfer bit enforced; restore/purge bits retired)', state: 'removed', - note: 'ADR-0049 → roadmap M2. #1883: the ops still do not exist in ObjectQL. `transfer` stays pre-mapped (OPERATION_TO_PERMISSION transfer→allowTransfer, modifyAllRecords bypass) and `allowTransfer` is ENFORCED today through the insert/update owner_id door (#3004). `allowRestore`/`allowPurge` RETIRED 2026-08-26 (#12497, maintainer ruling accepting #1883 rec B): the bits are retiredKey tombstones and their pre-mapping rows retired with them — a dispatched restore/purge is denied unconditionally via the DESTRUCTIVE_OPERATIONS fail-closed backstop (not even modifyAllRecords reaches it), so there is still no ungated window; the keys + rows + ops return in one M2 batch. Unit-proven in plugin-security/security-plugin.test.ts.' }, + note: 'ADR-0049 → roadmap M2, which builds the lifecycle ops and their RBAC bits as one batch: the ops still do not exist in ObjectQL. `transfer` stays pre-mapped (OPERATION_TO_PERMISSION transfer→allowTransfer, modifyAllRecords bypass) and `allowTransfer` is ENFORCED today through the insert/update owner_id door. `allowRestore`/`allowPurge` RETIRED 2026-08-26 (maintainer ruling: retire the two bits now rather than carry them unenforceable until M2): the bits are retiredKey tombstones and their pre-mapping rows retired with them — a dispatched restore/purge is denied unconditionally via the DESTRUCTIVE_OPERATIONS fail-closed backstop (not even modifyAllRecords reaches it), so there is still no ungated window; the keys + rows + ops return in one M2 batch. Unit-proven in plugin-security/security-plugin.test.ts.' }, ]; diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index 406c73d1729..caeb9f43722 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -247,54 +247,6 @@ "#5973": 1, "#6428": 1 }, - "packages/qa/dogfood/test/authz-conformance.matrix.ts": { - "#10145": 1, - "#10243": 1, - "#10982": 3, - "#11089": 1, - "#11373": 1, - "#11757": 1, - "#12176": 1, - "#12497": 1, - "#1883": 2, - "#1884": 1, - "#1888": 2, - "#1901": 2, - "#1994": 1, - "#2077": 1, - "#2567": 3, - "#2698": 1, - "#2937": 6, - "#2948": 2, - "#2956": 1, - "#2982": 1, - "#2992": 1, - "#3003": 3, - "#3004": 3, - "#3022": 1, - "#3043": 2, - "#3167": 1, - "#3801": 1, - "#3963": 1, - "#4983": 1, - "#5519": 4, - "#5561": 1, - "#5591": 1, - "#7023": 1, - "#7033": 1, - "#7665": 4, - "#7685": 3, - "#7792": 1, - "#7807": 1, - "#7900": 1, - "#7976": 2, - "#8613": 3, - "#8710": 1, - "#8711": 1, - "#8811": 1, - "#9083": 2, - "#9377": 1 - }, "packages/services/service-analytics/src/analytics-service.ts": { "#3867": 1, "#5222": 1,