Repository navigation
fix(plugin-audit): a read of the compliance ledger returns only the rows about records the caller can read (#21175) - #21194
Conversation
…the caller can read The compliance ledger takes the activity stream's parent-record read gate. The gate's mechanism moves into parent-record-read-gate.ts, shared by both streams; the ledger declares the rows that are about no record (run-level events) outside the gate's class. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…or serves on its own function The ledger's record gate now excludes, for every non-system caller, the rows about a deleted record and the rows naming a record it cannot judge. The redaction's handling of those rows is pinned on redactAuditLogRows over the row at rest; the read path pins that they are not served. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
… data doors A ledger reader is not served the rows about a record the data plane answers it 404 for, through the list, by-id, query and grouped-count doors; its own record's rows and a row about no record are served; the admin is the control. The field-values pin serves the live record's rows only, since the deleted record's rows now reach no non-system door. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…text early return Row 45 names the ledger's parent-record read gate beside the activity stream's; the declared counts are regenerated by gen:system-context-census. The changeset states the gate and its row classes. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…dger-parent-read-gate
📓 Docs Drift CheckThis PR changes 1 package(s): 43 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 150c6c20ebc0f0ec0772cff19b149ffe7d911ff3 && git checkout 150c6c20ebc0f0ec0772cff19b149ffe7d911ff3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7c5a311a5829ae3b550a3eeb9bb984f06be8b865 d376985f37a411720a0ef33e4acc86ad137d4a36 && git checkout -B drift-repro 7c5a311a5829ae3b550a3eeb9bb984f06be8b865 && git merge --no-ff d376985f37a411720a0ef33e4acc86ad137d4a36
node scripts/docs-audit/affected-docs.mjs --json 7c5a311a5829ae3b550a3eeb9bb984f06be8b865
|
…dger-parent-read-gate # Conflicts: # content/docs/permissions/system-context.mdx
…owing it is, and its mount order The changeset takes the activity stream gate's form: minor, a BREAKING paragraph, Clause-② no (narrowing), the ADR-0087 not-required marker and a Migration paragraph. The mount comment states the order on the ledger after the query guard landed: query guard, field redaction, parent-record read gate. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
… only to a caller who can read its record The Reading-the-trail paragraph said ledger queries go through the data service like any other object. They now take the ledger's parent-record read gate: a view of a record the reader cannot open, or of one since deleted, stays stored and is served only to system-context reads. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ubject user, as the ledger's parent-record gate requires Under the ledger's parent-record read gate a reader is served the rows about a user only when it can read that user. The pin's readers could open only their own user row, so they were served none of the subject's rows and the armed check disarmed. Each reader's sets now add view-all on the user object (a row-scope grant; the field classes still apply, measured by the armed check), and a new armed control asserts every reader opens the subject through the data door. No assertion changed. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…mpts its holder from the parent-record read gate; platform administrators hold it by default (objectstack-ai#21260) (objectstack-ai#21296) Fixes objectstack-ai#21260 Clause-②: yes (widening) This executes ruling B on objectstack-ai#21175 (comment `5942331027`, maintainer 「同意264」). The compliance ledger gets an audit capability, `view_all_audit_log`. A caller who holds it is exempt from the ledger's parent-record read gate. Field-level narrowing (PR objectstack-ai#21171) still applies to that caller. Platform administrators hold it by default. Every other position holds it only by explicit grant. objectstack-ai#21175 is the parent card. This PR does not close it (it is already closed). **Status.** Draft. `Clause-②: yes`, so an isolated contract-review-tier review is owed before enqueue. The PM runs it. ## Cross-lane surfaces, named before the change list - **`packages/spec/src/security/capabilities.ts`** (`domain:spec`). One new `PLATFORM_CAPABILITIES` entry. This is cross-lane, as the claim declares. - **`packages/spec/src/identity/eval-user.zod.ts`** (`domain:spec`). This file holds the default grant to platform administrators. - The claim expected the admin grant path to live in `plugin-security`. Measured: it lives here. `ADMIN_FULL_ACCESS_CAPABILITIES` is the one list that two consumers read: - plugin-security's `admin_full_access` set spreads it; - core's configured-owner envelope (`resolve-authz-context.ts`) reads it. - Adding the capability anywhere else would fork that list. It is reported here as the claim requires: "If that path lives outside `plugin-security`, the build reports it before touching it." - **`packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts`**. Test only, no source change in `plugin-security`. - Its pin on the admin set's exact capability list goes red on any capability change, by design. The literal now names the new capability, and its docblock says why. - A new pin says that no other shipped set carries the capability. ## What changed 1. **The declaration.** `PLATFORM_CAPABILITIES` gains `view_all_audit_log`: - label "View All Audit Log", `scope: 'org'` (see "Scope" below); - its description names exactly what it lifts and what still applies. - `PLATFORM_CAPABILITY_NAMES` derives from the list, so the seeder (`bootstrapSystemCapabilities`), the authoring lint and the anchor floor pick it up without a second edit. 2. **The default holder.** `ADMIN_FULL_ACCESS_CAPABILITIES.systemPermissions` lists it. Platform administrators hold it both ways: - through the `admin_full_access` grant; - through the configured-owner envelope. - There is no role-name test anywhere. 3. **The exemption.** `parent-record-read-gate.ts` is the shared mechanism. It gains one declared field, `ParentRecordGate.exemptCapability`, and one check at the top of `computeParentRecordFilter`, before anything is scanned or ANDed in. - The check reads the caller's resolved `systemPermissions`: the capability set that the request's authorization resolver (`resolveAuthzContext`, `assembleExecutionContext`) stamps on the execution context. - Every other runtime capability check outside plugin-security reads that same set, for example the `sys_record_share` read scope and the object-schema mask exemption. No new resolver is added. - Only the ledger gate declares the field (`LEDGER_AUDIT_CAPABILITY` in `audit-log-read-visibility.ts`). The activity stream's gate declares none and says so. 4. **The anchor floor (`high-privilege.ts`).** No edit. That file's platform floor is `PLATFORM_CAPABILITY_NAMES` itself, so declaring the capability puts it on the floor. - Reading: it unlocks ledger rows that the parent-record gate otherwise withholds. So the anchor-laundering argument applies, and a set carrying it must never bind to `everyone` or `guest`, even when an app declares a capability of the same name. - A pin in `high-privilege.test.ts` measures this. It refuses for `everyone` and for `guest`, with the name declared. 5. **Docs.** `content/docs/permissions/record-view-auditing.mdx`, the ledger reader sentence, which PR objectstack-ai#21194 wrote. It now names the capability, who holds it by default, the field-level narrowing, and the organization bound. Old and new text are in the report. 6. **Pins made false by this ruling.** - `audit-log-parent-read-gate.dogfood.test.ts` said the admin is not served a deleted record's rows. The admin is now a default holder: that pin now asserts the reader is served none of them and the admin is served both. - The comment in `audit-log-field-values.dogfood.test.ts` is corrected the same way. 7. **Changeset.** `@objectstack/spec` minor and `@objectstack/plugin-audit` minor, carrying `Clause-②: yes (widening)`. ## Measured before the change (the gate as landed) Real boot through the public data doors: `bootStack`, the real SecurityPlugin, the SQL driver, REST, auth, and AuditPlugin's CRUD mirror and auth-event sink. - **At base `d2bc644f2`, with `dist` built from that tree.** A member holding the ledger grant was served: - none of the deleted record's rows; - `404` on the sign-out row; - `404` on a private record's row; - a total of 2 against 2,053 at rest on a broad read past the 2,000-row bound. - The platform administrator was served none of the deleted record's rows. Its resolved capability set held the seven existing capabilities. - **With only the new exemption line removed, so the gate is exactly as objectstack-ai#21194 landed it.** Platform administrator and ledger-grant member, both: - deleted-record list 0; - `delete` row `404`, sign-out row `404`, ended session's sign-in row `404`; - broad read total 2 against 2,054 at rest. - This is A, as the dispatch expected. ## Scope: `org`, measured rather than assumed - The dispatch default was `platform` unless the build measured that the ledger is org-scoped. It is. - The registry provisions `organization_id` on `sys_audit_log`. - The CRUD mirror stamps each row with the record's organization. - Measured on a real `isolated` boot, with two organizations each created by its owner and the first owner granted the capability (the readings below are row counts): | Caller | Rows about the deleted record in its own org | Rows about the other org's deleted record | |---|---|---| | Holder (org A owner) | 2 | 0 | | Non-holder (org B owner) | 0 | 0 | | Platform administrator | 2 | 2 | - The capability lifts only the parent-record gate. The tenant wall still bounds a holder to its own organization's rows. The platform administrator reaches every organization through its own wall bypass, not through this capability. - This is now a pin, in the second block of the new dogfood file. ## Pins | Pin | Where | |---|---| | A holder is served the deleted-record rows (list and by id), the sign-out row, the ended session's sign-in row, a record it cannot open, and a broad read past the bound whole. | dogfood | | Field narrowing still applies to the holder: a withheld field's value is absent from the deleted record's served snapshots, a plain field's value is present, and the value is present at rest. | dogfood | | A non-holder (ledger grant only) gets exactly A: none of these rows, and a broad-read total below the bound. Its own record's rows are the control. | dogfood | | A platform administrator holds the capability by default, read through `resolveAuthzContext`, and is served the deleted-record, sign-out and broad-read rows. | dogfood | | The activity stream's gate is not exempted for a holder or for the admin. | dogfood; unit `activity-read-visibility.test.ts`; integration `audit-log-read-visibility.integration.test.ts` (real SQLite driver) | | The holder is bounded by its organization. | dogfood, second block | | On every read operation the holder is not narrowed and is not pre-scanned. Its broad read never meets the bound and logs no warning. A caller with other capabilities, a non-list or none gets the gate unchanged. The constant is pinned against `PLATFORM_CAPABILITIES` and `ADMIN_FULL_ACCESS_CAPABILITIES`. | unit `audit-log-read-visibility.test.ts` | | The holder is served every row, deleted and unjudgeable rows included, and the count matches. | integration | | The capability is on the anchor floor. | `high-privilege.test.ts` | | The admin grant carries it, declared `org`. | `platform-admin-capabilities.test.ts` | | Only `admin_full_access` among the shipped sets carries it. | `default-permission-sets.test.ts` | The pre-scan bound is pinned with a real fixture past it: 2,050 inserted rows plus the real ones. The constant is not shrunk. ## Ablations Each leg: mutate through `scripts/ablation-replace.mjs` (anchor must hit), run the src-aliased plugin-audit suites, rebuild `@objectstack/plugin-audit`, prove the mutation reached `dist/` with `scripts/ablation-dist-preflight.mjs`, run the dogfood file, restore with `git checkout HEAD -- ABSOLUTE_PATH` (blob hash equals HEAD and `git diff HEAD` is empty), rebuild, and prove the restore in `dist/`. A trap restores on EXIT, INT and TERM. Every leg turned red, as predicted. | Leg | Unit and integration | Dogfood | |---|---|---| | Exemption removed | 3 red: 2 unit holder pins, 1 integration holder pin | 8 red: every holder and admin pin, including the org-bound holder and admin pins. Non-holder and activity pins stayed green. | | Exemption applied to the activity gate | 2 red: unit activity pin, integration activity pin | 1 red: the activity pin | | Capability held by everyone | 19 red: the non-holder pin, plus every pre-existing ledger narrowing case | 2 red: both non-holder pins | The first run of the activity leg was a no-op. The replacement contained its own anchor, and the tool refused it with "anchor count moved 1 to 1". Its readings are void. It was re-run with an anchor the replacement does not contain, and the readings above are from that run. ## Gates and tests, at `d0a1903b7` - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 110 families. All 110 exited 0. - Two first answered `PREREQUISITE NOT MET` (exit 3) because sibling packages had no `dist/`: `check:skill-examples` and `check:dual-build-cjs-loads`. Both were re-run after building those packages and exited 0. - `--ran` reconciliation: "110 derived famil(ies) accounted for — 110 run, 0 NOT-MEASURED (a DERIVED zero — all 110 recorded an exit code and none of them is 3)". - **Spec artifacts.** `pnpm --filter @objectstack/spec check:generated`: "All 15 generated artifacts are up to date". Nothing was regenerated, because no artifact moves for one new list entry. - **Tests.** - `@objectstack/spec`: 645 files, 18,316 passed. - `@objectstack/plugin-security`: 157 files, 3,407 passed. - `@objectstack/plugin-audit`: 35 files, 542 passed. - `@objectstack/core` `resolve-authz-context.platform-admin-config.test.ts`: 31 passed. - Dogfood, 4 ledger files at this head: 37 passed. - **Typecheck.** `typecheck` passed for spec, plugin-audit, plugin-security and dogfood, including each package's test-layer typecheck. - **Lint.** ESLint on the 14 touched TS files with `--no-inline-config --format json`: 14 files linted, 0 errors, 0 warnings. - The repo's `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`). So this diff cannot change the verdict on any untouched file. - The full `pnpm lint` is left to CI. ## Acceptance notes - **Agent principals (ADR-0090 D10).** The exemption reads `systemPermissions` as the shared resolver stamped them. An OAuth agent principal carries the delegating user's capabilities only when the user consented to `actions:execute`, and none otherwise (`assemble-execution-context.ts`). The `sys_record_share` read scope and the object-schema mask exemption read the same set and follow the same rule. This is a reading, recorded for the contract reviewer. It is not a defect filed by this PR. - **What a holder is served.** A holder is served rows about records of objects it holds no object-level read on. Field-level security still narrows the snapshots. This is the breadth "exempt from the parent-record gate" means, and the capability's description and the docs page both say so. - **`PLATFORM_ADMIN_ONLY_CAPABILITIES` in plugin-security is deliberately untouched.** The new capability is not a platform-admin marker: it is org-scoped, and an explicit grant must not confer platform standing. - **Base.** `origin/main` has moved since `d2bc644f2`. The incoming commits touch none of this PR's files, so they were not merged here. CI and the queue validate the merge ref. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ion run proved stale (objectstack-ai#21339) Docs-only checklist revision from the 17.6.0 release-verification run objectstack-ai#21330 (subject `617f25f8`, Console pin `31971ff1e28f`). Every change follows the checklist README's lifecycle rule: the item's `revision` bumps and one `history` entry says what changed, why, and cites the run. No product code, no `content/docs/**`, no generated file. ## Stale clauses the run proved (each a FAIL in objectstack-ai#21330 with disposition stale-clause / assertion-defect) | item | rev | evidence | changed by | |---|---|---|---| | `access-security.audit-log-browser` | 2 → 3 | admin `GET /data/sys_audit_log?filter={"action":"delete"}` → 0 rows; the row is stored with correct attribution | `30c530e5` (objectstack-ai#21194): the ledger serves a non-system reader, admins included, only rows about records it can read | | `api-backend.filter-comparand-conformance` | 2 → 3 | POST `/query` → 400 `VALIDATION_FAILED` at `query.where.f_number.$eq`; GET `$filter` and engine → 400 `INVALID_FILTER`; no door returns rows | objectstack-ai#20116 (`cfc3bcf1` objectstack-ai#20247, `dd1b8031` objectstack-ai#20325) — the split query-contract-matrix rev 3 already records | | `api-backend.date-range-preset-matrix` | 1 → 2 | equality `{"signed_on":"today"}` → 400 `INVALID_FILTER` (temporal door); `$gte:"this_week"` → 400 with `bareDateRangePresetComparandMessage` | by design: `18.filter-preset-ordering-comparand-refused.ts` judges ordering positions only | | `records-forms.import-transform-matrix` | 1 → 2 | 400 `UNSUPPORTED_TRANSFORM` names the missing sandbox, 0 rows — but no `framework#2611` | `f115b1f` (objectstack-ai#21188): refusals state decisions in words, not tracker numbers | | `studio-authoring.view-authoring-live` | 1 → 2 | `GET /meta/view?object=repair_asset` serves `repair_asset.default` / `repair_asset.form` with the authored config; container name 0 hits | by design: `expandViewContainer` (objectstack-ai#7163, objectstack-ai#7736, objectstack-ai#13407) | ## Expected-fail notes 17.6.0 has made pass (clauses held in objectstack-ai#21330; only their framing was stale) | item | rev | measured | fixed by | |---|---|---|---| | `automation.packaged-flow-subflow-disable-refusal` | 1 → 2 | caller off → child's disable retry 200, ledger `active=false`; caller-first enable 409 `RESOURCE_CONFLICT` | `36d043b` objectstack-ai#20724, `0d9349f` objectstack-ai#20759; the enable guard is `679f95e` objectstack-ai#20711 (step 6 now enables the child first) | | `automation.packaged-flow-clone-contract` | 1 → 2 | clone survives a cold restart and fires; still unreachable from Studio | durability `cb4c31d` objectstack-ai#20907; reachability now filed as objectstack-ai#21332 (clause unchanged, still expected to fail) | | `access-security.packaged-flow-write-door-parity` | 1 → 2 | `PUT` / `DELETE /automation/showcase_urgent_task_alert` → 403 `NOT_OVERRIDABLE`, flow unchanged | `4b45afae` (objectstack-ai#20817); knownGap names the existing pin `packaged-flow-write-door-parity.dogfood.test.ts` | No clause was weakened: each still refuses the original failure mode (rows returned, a served delete row, a 200-with-zero-rows), and the clone clause keeps its expected fail. ## Validation - `node scripts/check-platform-checklist.mjs` → `OK — 15 areas, 269 items (265 active, 2 planned)`; symbol anchors and line-citation sweep green. - `api-backend.json` is re-serialized in its existing canonical 2-space form; the other four files are edited in place in their existing mixed formatting. Not in this PR (listed on objectstack-ai#21330's close-out instead): the other checklist-accuracy findings the run collected, and the two `planned` picklist items, which can only be promoted by a run in which they pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21175
Clause-②: no
A read of the compliance ledger (
sys_audit_log) now returns only the rows about records the caller can read. This implements triage's ruling A on the card (5932888473): the ledger takes the activity stream's parent-record read gate (#20833, PR #21069), and that gate reads the engine's own answer.Status. Draft. The product consequence for admins (see Acceptance notes) is awaiting the maintainer's decision on #21175; this PR's behaviour is unchanged by patch round 1.
Patch round 1: what changed since the first push
Merged
origin/mainat1ecb871b(merge commit77d3cc05; no rebase, no force-push). PR fix(plugin-audit,plugin-approvals)!: refuse a query over activity text, ledger snapshots or the approval snapshot for a reader withheld a field of its record (#21154) #21179 (security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154) is in that merge. Itsaudit-plugin.tsmount and this PR's mount sat in different hunks of the same block and merged without a conflict. The census page (content/docs/permissions/system-context.mdx) did conflict: row 45 now names both the query guard and the ledger read gate, and the declared counts were regenerated bygen:system-context-census(114 sites).Mount order. Engine middleware runs in registration order. On
sys_audit_logthe order is now: (1) the query guard from security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154, (2) the ledger field redaction from security(plugin-audit): the compliance ledger's create/update rows serve a withheld field's stored value in their before/after snapshots to a reader whose sets grant the ledger read, while the data plane serves that reader without the key #21155, (3) this PR's parent-record read gate. Onsys_activityit is: (1) the query guard, (2) the activity read gate, (3) the activity field redaction. Each guarantee holds:The mount comment in
audit-plugin.tsstates this order.Changeset, now in the activity gate's form (plugin-audit: sys_activity has no parent-record read gate, so any object-level read opens every activity row in the environment. Add the same read filter sys_comment has, keeping rows whose parent record the caller can read #20833 / PR fix(plugin-audit)!: an engine read of sys_activity returns only the rows whose parent record the caller can read #21069):
minor, a BREAKING paragraph, andClause-②: no (narrowing). The arm is there because this narrows what a read returns and no accept set widens; the PR line above keeps the claim'sno. It carries the ADR-0087not-required (no-migration-prescription)marker (check-adr-0087-registration: 1 declared-breaking changeset, carrying its disposition) and a Migration paragraph.CI red on the previous head. The red was
Dogfood Regression Gate (3/3); reproduced locally on that head. The failing case wasadmin-ledger-decision-metadata.dogfood.test.ts, a pin security(plugin-auth): the compliance-ledger row an admin create-user writes carries a withheld user field's value in its decision metadata, which a ledger reader withheld that field is served #21174 landed onmainafter this branch's first merge. Its readers could open only their own user row through the data door (measured: each reader got 404 on the subject user). Under the parent-record rule they are served none of the subject's ledger rows, so its armed check disarmed. Old to new: the fixture's reader sets now add view-all on the user object, a row-scope grant only. A new armed control asserts that every reader opens the subject through the data door (measured 200 for all five). No assertion changed: its armed check still measures that each reader is withheld exactly its field class, and all 8 cases pass.Docs.
content/docs/permissions/record-view-auditing.mdx, "Reading the trail": the sentence saying ledger queries go through the data service "like any other object" now states the read rule. A view row is served outside system context only to a caller who can read its record. Views of a record that has since been deleted stay stored and are served only to system-context reads.Measured first, on a real boot (classes only)
The re-measure was private and its readings stay in the dispatch's scratch. It ran on
mainatb9087d77(PR #21171 in), and again at this branch's first head. The stack wasbootStack, org-bound, with the realSecurityPlugin, auth, REST andAuditPlugin. The rows were written by the CRUD mirror and the auth-event sink. The member holds the ledger read through one explicit permission set; the admin is the seeded admin. "Parent" means the same caller's read of the row's record through the data door.GET /data/sys_audit_logloginrows)GET /data/sys_audit_log/:iddeleterows, a deleted record's other rows,logoutrows)totaltotalWhat changed
parent-record-read-gate.ts(new): the activity gate's mechanism, moved out ofactivity-read-visibility.tsand parameterized per gate. It still asksresolveReadableParentIds, the one readability answer the comment and activity gates share. Nothing derives row scope a second way.activity-read-visibility.ts: now a thin declaration over the shared module. Its exports (parseActivityParentObject, which the security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154 guard imports, included), scan options, sentinel and log lines are unchanged, and its unit and integration pins pass unedited.audit-log-read-visibility.ts(new):installAuditLogReadVisibility, the ledger's declaration, with the row classes below.audit-plugin.ts: one mount with its order comment, plus the no-middleware-seam warning now names the ledger read gate.content/docs/permissions/system-context.mdx, row 45: the census anchor for the new early return on system context.Row classes: the stated answer for rows the gate cannot judge
Measured per writer:
object_name+record_id): the CRUD mirror's create, update and delete rows; record-viewreadrows; plugin-auth's administrative create and update on a user; andlogin/logout, which name the session. Judged by the record gate.deleterow, everylogoutrow (sign-out deletes the session, measured), and a sign-in row whose session has since been removed. The rows stay stored.record_id, and an action that is not a record action): the run-levelimport,config_changeandplatform_admin_standing_changerows, and an auth event without a session id. These are outside the gate's class and are served under the ledger's own grant, as before. Why this differs from the activity gate, which excludes every row that names no parent:config_changesview, pinned by the settings dogfood test as the admin);audit-log-field-redaction.ts).create,read,update,delete) that names no record, a record under an object the engine does not know, and a row naming the ledger itself.Pins
audit-log-read-visibility.integration.test.tsuses a real kernel, the realAuditPlugin, the real CRUD mirror and a real SQLite driver. It covers find, findOne, count, aggregate, a scoped query, the batching count, every row class, and an admin control. 12 tests.audit-log-read-visibility.test.tscovers the class rule, the fail-closed branches, the scan bound and its order pass-through, and the inert seam. 13 tests.packages/qa/dogfood/test/audit-log-parent-read-gate.dogfood.test.tscovers the public doors above on a real boot. Each returned row's record is opened through the same door as the same reader. It also covers a row about no record from its real producer (a settings write), a deleted record's rows, and the admin control. 8 tests.redactAuditLogRowsover the row at rest. Its field-values dogfood pin checks the live record's rows.Ablation: put the forbidden behaviour back, red, restore
Both mutations ran from committed state through
scripts/ablation-replace.mjs, and each restore was proven: blob equals HEAD andgit diff HEADis empty.installAuditLogReadVisibility(...)call inaudit-plugin.ts; anchor 1 → 0).dist/: plugin-audit was rebuilt, andablation-dist-preflight --absentproved the call gone. The dogfood pin then gave 5 failed, 3 passed of 8.Verification at HEAD
d376985fEvery exit code was captured before any pipe.
pnpm --filter @objectstack/plugin-audit test: 35 files, 535 tests passed.pnpm --filter @objectstack/plugin-audit typecheck: exit 0, test layer included.pnpm --filter @objectstack/dogfood typecheck: exit 0.plugin-approvalspayload-predicate-guard.test.ts: 20 passed.OS_TEST_SHARD=3/3, CI's slice): 54 files passed, 1 skipped; 523 tests passed, 2 skipped.audit-log-field-values; security(plugin-audit): an activity row composed at write time may carry a changed field's values to a reader who can read the parent record but not that field (unmeasured; measure first) #21081'sactivity-field-values; security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154'sactivity-text-predicateandaudit-log-admin-search;activity-parent-read-gate;auth-session-audit-trail;settings-config-change-audit;admin-identity-audit-trail; security(plugin-auth): the compliance-ledger row an admin create-user writes carries a withheld user field's value in its decision metadata, which a ledger reader withheld that field is served #21174'sadmin-ledger-decision-metadata(8 of 8 after the fixture change);comments-permission-matrix;membership-actor-attribution. All green: eleven ran atb415f4d7, whose tree differs from this head only in the admin-ledger pin, and that pin ran at this head.dispatch-gates --commands: 94 families derived; 93 ran with exit 0 and 1 is NOT MEASURED.spec check:skill-examplesexited 0 after the client SDK it reads was built.check:dual-build-cjs-loadsexited 3, PREREQUISITE NOT MET: it needs a whole-repo build, and CI runs it.dispatch-gates --ran: 94 accounted for, 0 unrun.mainmoved again after1ecb871b; this round mergesmainonce, as dispatched.eslint --no-inline-config --format jsonover the 10 changed TypeScript files reports 10 files linted, 0 errors and 0 warnings. An ignored file would report a warning.eslint.config.mjssets noparserOptions.projectand no typed rule, so this diff cannot move a verdict on an untouched file. The whole-repopnpm lintruns in CI.Acceptance notes
domain:enginereports (5936411631on security(plugin-audit): sys_audit_log has no parent-record read gate, so a ledger reader is served the rows about a record the data plane answers 404 to (the ledger's #20833) #21175) that this gate shrinks [security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197's reach to admins: the gate stops serving a member the ledger rows about a key-material record that member cannot open. The admin half of [security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197 stands, and that card's own mechanism is separate.totalthey would add, are omitted. A record timeline scopes byobject_nameandrecord_idand never reaches the bound.GET /search, the analytics dataset over the ledger, export), and a boot with more than one organization.Generated by Claude Code