Skip to content

fix(plugin-audit)!: serve the ledger’s deployment-level rows to the platform administrator alone, in every posture - #28047

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-edzevh
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-edzevh

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #26750

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: updated here

维护者速读

  • 改了什么:合规审计账本里不属于任何组织的部署级记录(tenant_id 为空:全局设置变更、用户导入、平台管理员身份变更、管理员建号与重置密码),在所有租户姿态下(含 single)只给平台管理员看;其他读者,包括 view_all_audit_log 的持有者,只看属于某个组织的记录。
  • 为什么改:落实 [Decision] Under the single posture, does an organization-scoped audit-ledger reader also read deployment-level ledger rows, or are those for the platform administrator only? #26750 的裁决 A。single 姿态下组织行策略被剥离,任何拿到账本读权的人都能读到部署级记录。
  • 风险与回滚:读范围收窄(BREAKING)。single 部署里自建的审计角色和组织管理员将看不到部署配置历史;记录仍在库,系统上下文的导出不受影响。回滚即 revert 本 PR,无数据迁移。
  • 先看:packages/plugins/plugin-audit/src/audit-log-read-visibility.ts(中间件末尾新增的一行规则与 readsDeploymentLevelLedgerRows)、packages/plugins/plugin-audit/src/audit-log-deployment-level-rows.integration.test.ts、packages/qa/dogfood/test/admin-ledger-decision-metadata.dogfood.test.ts(按裁决翻转的钉子)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-audit exec vitest run src/audit-log-deployment-level-rows.integration.test.ts,预期 single / group / isolated 三个姿态共 18 条全绿。
  • 需要你做的:无,席位落地。

What changes

  • The read rule (packages/plugins/plugin-audit/src/audit-log-read-visibility.ts). On every non-system engine read of sys_audit_log (find, findOne, count, aggregate), a reader that is not the platform administrator gets one more clause ANDed into its where: tenant_id is neither null nor empty. It is added after the parent-record gate, which plans its parents off the read's own where, and it reaches the holder of view_all_audit_log too, whom that gate exempts. The platform administrator is the carried PLATFORM_ADMIN posture rung (PLATFORM_OPERATOR_POSTURE, ADR-0095 D2/D3), which the unscoped admin_full_access grant derives. Positions, set names and capabilities are not read, and a context with no rung gets the clause. Writes, system-context reads and context-less programmatic reads are untouched. Which organization's rows an organization reader is served stays the row policy's answer (ADR-0131 D8). The module is not re-exported, so the package's public surface is unchanged.
  • The capability text (packages/spec/src/security/capabilities.ts). The view_all_audit_log description gains one sentence for single. Nothing else in packages/spec changes.
  • Docs. One sentence in content/docs/permissions/record-view-auditing.mdx.
  • Changeset .changeset/26750-ledger-deployment-level-rows.md: patch for both packages, the two declaration lines, a BREAKING banner, and an upgrade note for single operators who granted the ledger to someone who is not the platform administrator.

Why

Ruling A on #26750 (comment 6457448424): deployment-level ledger rows (no tenant_id, ADR-0131 D7) are served only to the platform administrator in every tenancy posture, single included, and an organization-scoped holder of view_all_audit_log reads only the rows about its organization(s). Under single the shipped row policy sys_audit_log_org is stripped (a platform tenant policy by provenance, ADR-0105 D3), so every reader with a ledger grant was served those rows.

On the ruling's "its superuser read bypass": the bypass (viewAllRecords) skips plugin-security's Layer 1 only and never reaches this plugin's read middleware. So the rule reads the carried rung that names the platform administrator, and no new capability is introduced.

Tests

  • audit-log-read-visibility.test.ts (unit):

    • the clause's shape, and a fresh tree per call;
    • the rung-only predicate: TENANT_ADMIN, MEMBER, EXTERNAL, a mis-cased rung, no rung, platform_admin as a position, and admin_full_access as a permission or a capability all answer false;
    • a non-holder gets the gate, then the clause;
    • a holder on every non-admin rung gets the clause alone, on find, findOne, count and aggregate;
    • the platform administrator, with or without the capability, gets no clause;
    • writes, system reads and context-less reads get none.
  • audit-log-read-visibility.integration.test.ts: the fixture rows now carry the tenant each real writer stamps. A new case serves the deployment-level rows to the platform administrator alone; the member, the other member, the organization admin and the holder are served none.

  • audit-log-deployment-level-rows.integration.test.ts (new): one boot per posture (single, group, isolated) with the real CRUD mirror. The organization holder and the organization admin are served their organization's rows and no deployment-level row, through find, count, a tenant_id: null filter, findOne by id and a grouped aggregate. The platform administrator is served both.

  • The dogfood pin sweep. Both files were named to the seat and added to the claim's file surface before they were edited.

    • audit-log-audit-capability.dogfood.test.ts: the broad read's bulk rows were system inserts with no tenant_id, which made them deployment-level rows. They now carry the holder's organization, as the CRUD mirror stamps a row about a record of it. The test's subject is unchanged.
    • admin-ledger-decision-metadata.dogfood.test.ts: a pin flip caused by ruling 6457448424, not a fix-to-green. Every row about the subject (plugin-auth's explicit rows and the mirror's rows, written by the platform administrator's endpoints with no organization) is deployment-level, so no class reader can be served one in any posture. The decisions-only guarantee is now asserted at the producer, on the rows at rest. The read side pins the ruling: every reader is served only a control row about the same subject that carries the readers' organization, and the platform administrator is served every row. The armed checks keep their shape: the rows exist and the mirror carries every class; every reader opens the subject; no row about the subject carries an organization, and every reader is served the control row.
  • Two plugin-audit fixtures the full package suite found (inside the claim's surface, "its tests under packages/plugins/plugin-audit/src/"):

    • audit-log-field-redaction.test.ts: the rows from other writers were system inserts with no tenant_id, so the organization readers there were no longer served them. They carry the Default Organization, as a tenant-scope writer stamps them. The redaction cases are unchanged.
    • parent-record-read-leaf.integration.test.ts: the rows about no record carry the Default Organization the same way. The observer that records each ledger read's WHERE "as the gates left it" takes the deployment-level clause, ANDed after the gate, off what it records, so the gate's own WHERE is still what is measured. The holder's case now also pins that the clause is there.

Acceptance notes

  • Measurement (the card's first step). Under group and isolated, a custom set that grants the ledger read and view_all_audit_log and omits sys_audit_log_org was measured on a real boot before the change. Its holder was served only its own organization's rows, so that set has no NULL-tenant leak to fix.
  • The cross-organization half is outside this card. The same measurement found that under a wall, a custom set that authors its own ledger row policy, or that grants the read bypass on the ledger, is served rows beyond its holder's organization. This change closes the deployment-level half of that: those rows are now the platform administrator's alone, in every posture. Rows about other organizations stay reachable through such a set. No reproduction detail is given here; the withheld detail stays on security(plugin-audit): a sys_secret confidentiality gap, observed beside the 18.2.0 wave-2 clauses — detail withheld pending maintainer #26640.
  • The ledger rows stay stored, and nothing is re-attributed (the ruling: not B).

Verification

Final head af925f22. It differs from 173abc30 only in the two plugin-audit fixtures above, which no dogfood file runs.

Ablation 1: the rule line replaced by a marker, three plugin-audit files: 20 red, restored
node scripts/ablation-replace.mjs --file packages/plugins/plugin-audit/src/audit-log-read-visibility.ts \
  --anchor 'if (!readsDeploymentLevelLedgerRows(ctx.context)) andIntoWhere(ctx, ledgerRowAboutAnOrganizationWhere());' \
  --replacement '// ABLATION: the deployment-level rule removed' -- pnpm --filter @objectstack/plugin-audit exec vitest run \
  src/audit-log-read-visibility.test.ts src/audit-log-read-visibility.integration.test.ts src/audit-log-deployment-level-rows.integration.test.ts
ablation-replace: ok mutation landed: anchor 1 -> 0, blob c49da4b4973d -> f05a69cb34ea
 Test Files  3 failed (3)
      Tests  20 failed | 44 passed (64)
red (count × title):
  3 × the organization holder of the audit capability is served its organization's rows, and no deployment-level row   (single, group, isolated)
  3 × the organization admin is served its organization's rows, and no deployment-level row                          (single, group, isolated)
  3 × a read that asks for the deployment-level rows by name or by id is served none of them                          (single, group, isolated)
  3 × a grouped count sees no deployment-level row                                                                    (single, group, isolated)
  1 × a caller without the audit capability gets the gate, then the clause ANDed after it
  1 × an organization holder of the audit capability, on every rung but the platform administrator's, gets the clause alone
  1 × its holder is not narrowed by the parent-record gate, on any read operation, and nothing is scanned for it
  1 × [#26750] the deployment-level rows are served to the platform administrator alone
  1 × [#21260] a holder of the ledger audit capability is served every row about the organization, deleted and unreadable records included
  1 × find: a member reads the rows about records it can read and the organization's rows about no record, and nothing else
  1 × aggregate: a grouped count sees only the readable rows
  1 × an admin, who reads every record, keeps every row about a record that exists and the organization's rows about none
ablation-replace: ok restored: blob == HEAD (c49da4b4973d) and `git diff HEAD` is empty
Ablation 2: the predicate also reads a platform_admin position: the rung-only pin red, restored
--anchor 'return context?.posture === PLATFORM_OPERATOR_POSTURE;'
--replacement 'return context?.posture === PLATFORM_OPERATOR_POSTURE || ((context as any)?.positions ?? []).includes("platform_admin"); // ABLATION: positions read beside the rung'
 Test Files  1 failed | 2 passed (3)
      Tests  1 failed | 63 passed (64)
  × the platform administrator is the carried PLATFORM_ADMIN rung, and nothing else
AssertionError: {"userId":"u1","positions":["platform_admin"]}: expected true to be false
ablation-replace: ok restored: blob == HEAD (c49da4b4973d) and `git diff HEAD` is empty
Ablation 3: the rule removed, against the re-armed leaf fixture: the holder's clause pin red, restored
 Test Files  1 failed | 1 passed (2)
      Tests  1 failed | 32 passed (33)
  × ledger: the audit capability's holder is not narrowed — the gate writes nothing for it
AssertionError: expected false to be true
ablation-replace: ok restored: blob == HEAD (c49da4b4973d) and `git diff HEAD` is empty
Package suites (green), each under the verify lock
pnpm --filter @objectstack/plugin-audit test                                         Test Files 66 passed (66)   · Tests 1077 passed (1077)
pnpm --filter @objectstack/spec exec vitest run src/security src/identity           Test Files 26 passed (26)   · Tests 601 passed (601)
pnpm --filter @objectstack/plugin-security test                                      Test Files 237 passed (237) · Tests 5119 passed | 45 skipped (5164)
plugin-auth: durability-swallow-repair + admin-user-endpoints                        Test Files 2 passed (2)     · Tests 115 passed (115)
service-settings: config-change-audit                                                Test Files 1 passed (1)     · Tests 17 passed (17)
service-automation: resume-decider-identity + runas-attribution-contract             Test Files 2 passed (2)     · Tests 23 passed (23)

The neighbouring suites are the ones outside plugin-audit that boot the audit plugin and read the ledger.

Full dogfood battery: red on the first head (the two fixtures, named above), green after
46f5ab64 · os-verify-lock: VERDICT command-exit 1 · held the lock 1175s
 Test Files  2 failed | 355 passed | 3 skipped (360)
      Tests  1 failed | 2866 passed | 28 skipped (2895)
  audit-log-audit-capability.dogfood.test.ts:352   expected 2055, received 5 (the bulk rows carried no tenant_id)
  admin-ledger-decision-metadata.dogfood.test.ts   assertArmed disarmed (no reader served any row about the subject)

173abc30 · pnpm --filter @objectstack/dogfood test
os-verify-lock: VERDICT command-exit 0 · held the lock 1118s (18m38s) · waited 0s
 Test Files  357 passed | 3 skipped (360)
      Tests  2877 passed | 20 skipped (2897)
dispatch-gates: 106 derived families, run and reconciled
173abc30: node scripts/pm/dispatch-gates.mjs --ran gates-ran.txt
  Run reconciliation — 106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN.
  ✓ dispatch-gates --ran: 106 derived famil(ies) accounted for — 106 run, 0 NOT-MEASURED (a DERIVED zero — all 106 recorded an exit code and none of them is 3).
  every family exit 0
af925f22: the same 106 families (re-derived, identical list) are re-running; the reconciliation goes in the card's report comment.

Three families needed a prerequisite in this shallow cloud clone before they measured: check-plugin-teardown-shape --self-test (the pinned fixture commit fetched), spec check:skill-examples (client SDK declarations built), check:dual-build-cjs-loads (six package builds).

Lint and generated artifacts
pnpm lint                                                  exit 0
pnpm --filter @objectstack/spec run check:generated        exit 0 · All 14 generated artifacts are up to date
node scripts/pm/check-governed-merges.mjs --branch …       NOT governed · 0 of 9 paths hit the register

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (The only spec change is one sentence in a capability description.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (check:generated: all 14 up to date; no artifact carries the description.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 block is the fleet's one sanctioned Chinese section.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Contract review is owed: the seat runs it on the final head.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cwC9jjzyBACUnk1V9V


Generated by Claude Code

…atform administrator alone, in every posture

Ruling A: a ledger row about no organization (tenant_id NULL, ADR-0131 D7) is
served only to the carried PLATFORM_ADMIN rung; every other non-system reader,
the view_all_audit_log holder included, reads through a tenant_id-present
clause ANDed after the parent-record gate. Under a wall the shipped row policy
already excluded them; this holds the line under single and for a set that
authors its own ledger policy or the superuser read bypass on the ledger.

Claude-Session: https://claude.ai/code/session_016WV4cwC9jjzyBACUnk1V9V
Co-authored-by: Claude <noreply@anthropic.com>
…nd the ledger read scope sentence in the record-view auditing page

Claude-Session: https://claude.ai/code/session_016WV4cwC9jjzyBACUnk1V9V
Co-authored-by: Claude <noreply@anthropic.com>
…vel rows as non-admins

The audit capability fixture's bulk rows now carry the holder's organization,
as the CRUD mirror stamps a row about a record of it; unstamped they are
deployment-level rows, served to the platform administrator alone.

The admin identity decision-metadata fixture's rows about the subject are all
deployment-level, so no class reader is served one in any posture. Its
decisions-only guarantee is asserted at the producer, on the rows at rest,
and its read side now pins the ruling: every reader is served only a control
row about the same subject that carries an organization, and the platform
administrator is served every row. The armed checks keep their shape.

The changeset's upgrade note and the docs examples name the admin
create-user and password-reset rows among the deployment-level rows.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwC9jjzyBACUnk1V9V
… served, and read the leaf gate's WHERE past the ruling's clause

The field-redaction fixture's rows from other writers, and the leaf fixture's
rows about no record, were system inserts with no tenant_id: deployment-level
rows, which no reader in either file is served now. They carry the Default
Organization, as a tenant-scope writer stamps them.

The leaf fixture's observer records each ledger read's WHERE as the gate left
it, with the deployment-level clause ANDed after the gate taken off, and the
holder's case pins that the clause is there.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwC9jjzyBACUnk1V9V
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-audit, @objectstack/spec, touching 5 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authorization.mdx (via PLATFORM_CAPABILITIES (symbol, a top-level const object))
  • content/docs/permissions/record-view-auditing.mdx (via view_all_audit_log (literal, a string literal in PLATFORM_CAPABILITIES))
  • content/docs/permissions/system-context.mdx (via installAuditLogReadVisibility (symbol, a top-level function))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-7.mdx (via view_all_audit_log (literal, a string literal in PLATFORM_CAPABILITIES))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 171 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 847e4248e9b7059e5b069056bf636c75c1f53b14 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2aefbcde88274fdb6de117cd9d2c7920b36d391e — the merge of head e94fcd240ecc95c3fecb352f7010958877dade4a into base 847e4248e9b7059e5b069056bf636c75c1f53b14, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2aefbcde88274fdb6de117cd9d2c7920b36d391e && git checkout 2aefbcde88274fdb6de117cd9d2c7920b36d391e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 847e4248e9b7059e5b069056bf636c75c1f53b14 e94fcd240ecc95c3fecb352f7010958877dade4a && git checkout -B drift-repro 847e4248e9b7059e5b069056bf636c75c1f53b14 && git merge --no-ff e94fcd240ecc95c3fecb352f7010958877dade4a

node scripts/docs-audit/affected-docs.mjs --json 847e4248e9b7059e5b069056bf636c75c1f53b14

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 847e4248e9b7059e5b069056bf636c75c1f53b14 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: af925f22afb66fc213fe7f4654e3a331367a94b3
Local-runs: none
Scope: packages/spec/src/security/capabilities.ts + packages/plugins/plugin-audit/src/audit-log-read-visibility.ts, .changeset/26750-ledger-deployment-level-rows.md (narrowing face, no remainder); cited code read on the live tree: packages/plugins/plugin-audit/src/parent-record-read-gate.ts, packages/plugins/plugin-audit/src/audit-writers.ts, packages/plugins/plugin-audit/src/auth-event-audit.ts, packages/plugins/plugin-audit/src/read-audit.ts, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/assemble-execution-context.ts, packages/core/src/security/posture-ladder.ts, packages/metadata-core/src/meta-write-capability.ts, packages/plugins/plugin-security/src/objects/default-permission-sets.ts, packages/plugins/plugin-security/src/security-plugin-rls-filter.ts, packages/plugins/plugin-auth/src/auth-session-audit.ts, packages/services/service-automation/src/run-resume.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-memory/src/memory-driver.ts, packages/objectql/src/engine.ts, docs/adr/0095, docs/adr/0131.

Check-runs on the head at review time: every completed run is success or skipped (Check Changeset, Governed Surface Queue Guard, Spec property liveness, all Lint and Type Check source/consumer/debt gates, Dogfood Regression Gate, Build Docs, Build UI Deps, Test UI, Build & E2E UI). Still in_progress: Test Core (8 shards), Type Check · workspace, Type Check UI, Live E2E UI, Build Core. The verdict below is on the diff, the card and the completed runs; the in-progress runs are the queue's gate, not this record's.

① Derived judgments

  • Accept-set, the ledger read. Every non-system engine read of sys_audit_log (find, findOne, count, aggregate: PARENT_GATE_READ_OPS) by a context that does not carry the PLATFORM_ADMIN rung gets tenant_id not null and not empty ANDed into its where, after the parent-record gate, which plans its parents off the caller's own where (andIntoWhere wraps, never replaces). Right: this is ruling A (6457448424) as written, one rule in every posture, including the holder of view_all_audit_log, whom the gate exempts and this clause does not.
  • Under a wall, shipped sets: no change. sys_audit_log_org (tenant_id == current_user.organization_id, default-permission-sets.ts:214) already excluded NULL-tenant rows for the shipped sets; the new clause is a subset of what that policy already removes. Right. Under a wall with a custom set that authors its own ledger row policy or the read bypass: the NULL-tenant half is now closed; the cross-organization half is declared out of this card and left on security(plugin-audit): a sys_secret confidentiality gap, observed beside the 18.2.0 wave-2 clauses — detail withheld pending maintainer #26640 (withheld). Right, and correctly declared in the changeset.
  • Platform administrator: unchanged where the rung is carried. readsDeploymentLevelLedgerRows reads context.posture === PLATFORM_OPERATOR_POSTURE ('PLATFORM_ADMIN', metadata-core). The rung is derived once, server-side, in resolve-authz-context.ts 6d (derivePosture from anchorsPlatformStanding grants or the config-derived admin; not wall-gated, so single derives it too) and carried by assemble-execution-context.ts (posture: authz.posture, authenticated principals only). No transport reads a posture off the request (grep of the HTTP, MCP and runtime doors). Not forgeable from the client. Right.
  • Fail closed on no rung. A context with no posture is treated as not the platform administrator. Narrower than the RLS filter's precedent (security-plugin-rls-filter.ts:194, which falls back to a capability probe) and the same as suggested-audience-bindings.ts:934. Direction right (never widens). Consequence named in ④ as should-fix: a runAs:'user' automation context is seeded with positions and permissions but not the rung (run-resume.ts:215), so a flow running as the platform administrator is served organization rows only.
  • Writes, system reads, context-less reads: unchanged. The middleware's existing early return covers them. Right.
  • Driver semantics of the clause. $ne: null lowers to IS NOT NULL on driver-sql (sql-driver.ts:14716) and excludes null and missing on memory/mongo (memory-driver.ts:1577); $ne: '' is NULL-safe on SQL and is ANDed behind the null test, so a NULL never reaches it. Same pattern as ledgerRowAboutNoRecordWhere in the same file. Right.
  • Stamping, the rule's premise. audit-writers.ts:2096: tenantId = recordOrgId ?? sess.organizationId; under single a session.create.before hook stamps activeOrganizationId (auth-manager.ts:835, ensure-default-organization.ts), so the CRUD mirror, record-view read rows and login/logout rows (auth-session-audit.ts:171/195, from session.activeOrganizationId) carry the Default Organization on the normal path. The residual NULL class (system writes about an object with no organization column, sudo paths with no session organization) was already hidden from organization readers under a wall and is now hidden under single too; the changeset's Before bullet names it. Right, and this is the ruling's own consequence, not a defect of the rule.
  • Public surface. audit-log-read-visibility.ts is not re-exported from the package index (only audit-plugin.ts imports it), so the two new exports are internal. packages/spec: one description string in PLATFORM_CAPABILITIES; no schema shape, no generated artifact carries the description (grep: only source and test files name view_all_audit_log); check:generated and Spec property liveness green. Right.
  • Dogfood pin flip (admin-ledger-decision-metadata.dogfood.test.ts, outside hits, read for the claim): every row about the subject is written by the platform administrator's endpoints with no organization, so under ruling A no class reader is served one; the decisions-only guarantee moves to the rows at rest, and a control row carrying the readers' organization proves each reader's ledger read resolved. A faithful flip caused by the ruling, not a fix-to-green. Right.

② Semver level

  • Clause-②: no (narrowing) on the PR body and in the changeset; Metadata-protocol: none on both. Right: a read narrows for a custom grant, nothing a metadata author can write changes.
  • Level: patch for @objectstack/plugin-audit and @objectstack/spec. Right by AGENTS.md Post-Task Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3: (narrowing) is BREAKING, a non-protocol breaking change ships patch with its BREAKING banner and ADR-0087 disposition; both present (**BREAKING** read narrowing …, adr-0087: not-required (no-migration-prescription)), plus the upgrade note for single operators. fix(plugin-audit)!: title. Check Changeset green.
  • The spec bump is description text on a published const: patch is the right level; no skip-changeset.

③ Boundary flags

  • Cross-organization half under a wall (custom ledger policy or read bypass). Flagged by the dev as outside this card; escalated to security(plugin-audit): a sys_secret confidentiality gap, observed beside the 18.2.0 wave-2 clauses — detail withheld pending maintainer #26640, where the detail stays withheld. Accepted; nothing of it is reproduced here.
  • The card's first step (measure group / isolated with a custom set omitting sys_audit_log_org). Reported done by the dev with no NULL-tenant leak found. Read-only here, so not re-measured; it does not bear on the rule's correctness, which applies in every posture independent of the row policy. Not pinned by a test in the tree (the new integration test boots without the security plugin, by design). Noted, not a finding.
  • Final-head dispatch-gates reconciliation. The PR body says the 106 families on af925f22 were re-running and the reconciliation goes in the card's report comment. At review time the card holds four comments and the last is the claim: the report is not there yet. Open until it lands (should-fix 3).
  • No open_questions entries; no TODO/FIXME added by the diff.

④ Design judgment

packages/spec/src/security/capabilities.ts (hits). The one added sentence states, for single, exactly what ruling A ordered the description to gain, and it does not contradict the sentence before it (walled postures, platform administrator). It holds against its first consumer: the plugin-audit middleware serves exactly that set, pinned per posture in audit-log-deployment-level-rows.integration.test.ts (find, count, a tenant_id: null filter, findOne by id, grouped aggregate, for the holder, the organization admin and the platform administrator). Scope 'org' unchanged. Tried to break it: the description still says the platform administrator reaches every row; that is true wherever the request path carries the rung, and the exception (contexts built outside the resolver) is a plugin-audit fact, so it belongs in the changeset, not in the capability text.

  • Should-fix 2. The code comment directly above the row (lines 70-80, unchanged by the diff) still says the platform administrator reaches deployment-level rows "through its superuser read bypass, not through this capability". After this change the bypass is half the mechanism: under a wall the bypass skips the row policy, and in every posture the plugin-audit rule admits the carried PLATFORM_ADMIN rung. One clause naming the rung keeps the comment and the new sentence telling the same story.

packages/plugins/plugin-audit/src/audit-log-read-visibility.ts (the read rule, in scope on the narrowing face). Does the narrowing make the spec's own refusal real? Yes: the description on main already promised that deployment-level rows are the platform administrator's; the rule now enforces it where no row policy did. Tried to break it: the rung cannot be supplied by a client; the clause is ANDed after the gate so the gate's parent planning is unchanged; count and aggregate ride the same AST (engine-count-aggregate.ts); a mis-cased rung, a platform_admin position, an admin_full_access permission or capability all answer false (unit pins, and ablation 2 in the PR body shows the position pin is live); findOne by id of a withheld row answers null, as the gate does. The only behaviour I could reach that the declaration does not name is the fail-closed case below, which is narrower, never wider.

  • Should-fix 1. A context assembled outside resolveAuthzContext carries no rung and is served organization rows only, the platform administrator included. The reachable instance in the tree is a runAs:'user' automation run (service-automation/src/run-resume.ts seeds positions, permissions, peers and email; not posture). Name it in the changeset's Unchanged bullet ("the platform administrator reads every row as before" holds only where the request path carries the rung) and either seed the rung in seedRunGrants on a follow-up card or state that a flow reading the ledger does so in system context. Fail-closed, so not a FAIL.

.changeset/26750-ledger-deployment-level-rows.md (in scope on the narrowing face). Declaration lines, BREAKING banner, ADR-0087 marker and upgrade note are present and match the diff. Before/Now/Unchanged bullets match the tree, with the one qualification in should-fix 1. The upgrade note's "The organization's own record history ... still served" is true for the normal stamped path; the residual NULL class is already listed in the Before bullet, so the note is complete enough.

Must-fix: none.
Should-fix: (1) changeset qualifies the platform administrator's "as before" with the carried rung and names the run-as case, with a follow-up to seed the rung or state the system-context read; (2) the capabilities.ts comment above the row names the rung beside the bypass; (3) the af925f22 dispatch-gates reconciliation lands on the card before the queue.

Implemented-by: claude/project-thread-edzevh
Reviewed-by: session_016WV4czVnUdQQCVKSW5J2uk

VERDICT: PASS

…nt-level rows on a request that carries its rung

The changeset's Unchanged bullet now says the platform administrator's read
holds on a request whose authorization context carries the PLATFORM_ADMIN
rung, and that a context built outside that resolution is served the rows
about an organization only, fail-closed. The comment above view_all_audit_log
names the rung beside the superuser read bypass.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwC9jjzyBACUnk1V9V
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3fa0b74303605db6aebb5b5c1e342e88b9babf53
Local-runs: none
Scope: packages/spec/src/security/capabilities.ts + packages/plugins/plugin-audit/src/audit-log-read-visibility.ts, .changeset/26750-ledger-deployment-level-rows.md (narrowing face, no remainder); cited code read on the live tree: packages/plugins/plugin-audit/src/parent-record-read-gate.ts, packages/plugins/plugin-audit/src/audit-writers.ts, packages/plugins/plugin-audit/src/auth-event-audit.ts, packages/plugins/plugin-audit/src/read-audit.ts, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/assemble-execution-context.ts, packages/core/src/security/posture-ladder.ts, packages/metadata-core/src/meta-write-capability.ts, packages/plugins/plugin-security/src/objects/default-permission-sets.ts, packages/plugins/plugin-security/src/security-plugin-rls-filter.ts, packages/plugins/plugin-auth/src/auth-session-audit.ts, packages/services/service-automation/src/run-resume.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-memory/src/memory-driver.ts, packages/objectql/src/engine.ts, docs/adr/0095, docs/adr/0131.

This head follows af925f22, reviewed PASS by this seat. The delta af925f22..3fa0b743 is one commit touching two files, prose only: the changeset's Unchanged bullet (one sentence qualified, one added) and the code comment above the view_all_audit_log row in capabilities.ts (one clause). No source, test, description string or generated artifact changes; the net diff against origin/main is the reviewed diff plus those two hunks (11 files, +565/-119). Every judgment below that the delta does not touch is re-stated from the reviewed record and still holds against the tree.

Check-runs on the head at review time: completed and success: Governed Surface Queue Guard, Check PR Size, Check Documentation Links; skipped: Schedule gate, Packed-tarball smoke, 18.x upgrade harness. Still in_progress or queued: Check Changeset, Spec property liveness, every Lint and Type Check gate, PR-body guards, Flag docs affected, Auto Label; the Build, Test Core, Test UI, Dogfood and E2E runs had not started. The delta cannot change any of their answers from af925f22, where every completed run was success or skipped, since it edits a changeset body and a comment, but their conclusions on this head are the queue's gate, not this record's.

① Derived judgments

  • Accept-set, the ledger read. Unchanged by the delta. Every non-system engine read of sys_audit_log (find, findOne, count, aggregate) by a context not carrying the PLATFORM_ADMIN rung gets tenant_id not null and not empty ANDed into its where after the parent-record gate (andIntoWhere wraps, never replaces). Right: ruling A (6457448424) as written, one rule in every posture, the holder of view_all_audit_log included.
  • Under a wall, shipped sets: no change. sys_audit_log_org already excluded NULL-tenant rows; the clause is a subset of what it removes. Right. The cross-organization half of a custom walled set stays on security(plugin-audit): a sys_secret confidentiality gap, observed beside the 18.2.0 wave-2 clauses — detail withheld pending maintainer #26640 (withheld). Right, declared in the changeset.
  • Platform administrator. The rung is derived once, server-side (resolve-authz-context.ts 6d, derivePosture from anchorsPlatformStanding grants or the config-derived admin, not wall-gated) and carried by assemble-execution-context.ts; no transport reads a posture off the request. Not client-forgeable. Right. The delta's changeset sentence now says exactly this: the administrator's read holds on a request carrying the rung. Right.
  • Fail closed on no rung. A context built outside the resolver carries no rung and is served organization rows only; the reachable instance is a runAs:'user' automation run (run-resume.ts seeds positions, permissions, peers, email; not posture). The delta names it in the changeset and leaves seedRunGrants untouched (declared out of scope by the dev). Narrower, never wider: right, and now declared.
  • Writes, system reads, context-less reads: unchanged. Right.
  • Driver semantics. $ne: null is IS NOT NULL on driver-sql and excludes null and missing on memory/mongo; $ne: '' is ANDed behind it. Right.
  • Stamping premise. Under single the session.create.before hook stamps activeOrganizationId, so CRUD, record-view and login/logout rows carry the Default Organization on the normal path; the residual NULL class is the ruling's own consequence and is named in the changeset's Before bullet. Right.
  • Public surface. The read-visibility module is not re-exported; the spec change is one description string carried by no generated artifact. The delta adds a comment only. Right.
  • Dogfood pin flip. Unchanged by the delta; a faithful consequence of the ruling, not a fix-to-green. Right.

② Semver level

  • Clause-②: no (narrowing) and Metadata-protocol: none on the PR body and in the changeset; unchanged by the delta. Right.
  • patch for @objectstack/plugin-audit and @objectstack/spec, **BREAKING** banner, adr-0087: not-required (no-migration-prescription) marker, upgrade note: all present; the delta edits the Unchanged bullet only and keeps every declaration line intact. Right by AGENTS.md Post-Task Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 (a non-protocol breaking change ships patch with its banner and disposition). Check Changeset was green on af925f22 and is in progress on this head.

③ Boundary flags

  • Should-fix 1 (reviewed record): resolved. The changeset's Unchanged bullet now reads that the platform administrator reads every row "on a request that carries the PLATFORM_ADMIN rung, which the request's authorization context resolves for the administrator", and that a context built outside that resolution is served organization rows only, fail-closed, with the runAs: 'user' run named. Matches the tree (run-resume.ts seeds no posture). Seeding the rung in seedRunGrants is left out of this PR and declared out of scope; acceptable, since the behaviour is narrower and now documented. No follow-up card number is cited on the PR; a one-line pointer when one exists would close the loop.
  • Should-fix 2 (reviewed record): resolved. The capabilities.ts comment above the row now says the deployment-level rows are served in every posture only to a request carrying the PLATFORM_ADMIN rung, naming [Decision] Under the single posture, does an organization-scoped audit-ledger reader also read deployment-level ledger rows, or are those for the platform administrator only? #26750 and the plugin-audit module, beside the existing superuser-bypass sentence. The comment and the description now tell one story. Comment only; the description string is byte-identical to af925f22.
  • Should-fix 3 (reviewed record): pending. The af925f22 dispatch-gates reconciliation promised for the card's report comment has not landed: the card still holds four comments, the last being the claim (6519222523). The gate families are unaffected by this delta (prose), so the reconciliation from af925f22 still describes this head's code; it belongs on the card before the queue.
  • PR body stale on the head. The Verification section still reads "Final head af925f22". The body is prose and not reviewed under ④, but the queue guard and the next reader read the head off it; update the line to 3fa0b743 (should-fix 4, minor).
  • Cross-organization half: escalated to security(plugin-audit): a sys_secret confidentiality gap, observed beside the 18.2.0 wave-2 clauses — detail withheld pending maintainer #26640, withheld there. Accepted. The card's ordered measurement: reported by the dev, not re-measured here (read-only), not load-bearing for the rule. No open_questions; no TODO/FIXME in the diff.

④ Design judgment

packages/spec/src/security/capabilities.ts (hits). The published row is unchanged from the reviewed head: the one added sentence states for single exactly what ruling A ordered, consistent with the preceding sentence, and holds against its first consumer, the plugin-audit middleware, pinned per posture in audit-log-deployment-level-rows.integration.test.ts. The delta's comment clause is accurate against the tree: the rung is PLATFORM_OPERATOR_POSTURE read in readsDeploymentLevelLedgerRows, applied in every posture, and the superuser bypass remains the mechanism that skips the row policy under a wall. Nothing left to break here; scope 'org' unchanged.

packages/plugins/plugin-audit/src/audit-log-read-visibility.ts (the read rule, in scope on the narrowing face). Byte-identical to the reviewed head. The narrowing makes the spec's own refusal real: the description on main already promised deployment-level rows to the platform administrator alone, and the rule enforces it where no row policy did. The rung cannot be supplied by a client; the clause is ANDed after the gate; count and aggregate ride the same AST; a mis-cased rung, a position, a permission or a capability all answer false (unit pins, ablation 2 live); findOne by id of a withheld row answers null as the gate does. The one behaviour outside the request path (no rung, fail-closed) is now declared in the changeset. No must-fix.

.changeset/26750-ledger-deployment-level-rows.md (in scope on the narrowing face). Declaration lines, banner, ADR-0087 marker and upgrade note intact. The Unchanged bullet now matches the tree in full, the qualification this seat asked for included. Before/Now bullets unchanged and still right. No angle-bracket fragments were introduced.

Must-fix: none.
Should-fix: (3, carried) the af925f22 dispatch-gates reconciliation lands on the card before the queue; (4, new, minor) the PR body's "Final head" line names 3fa0b743; (optional) cite the follow-up card for seeding the rung in seedRunGrants once one exists.

Implemented-by: claude/project-thread-edzevh
Reviewed-by: session_016WV4czVnUdQQCVKSW5J2uk

VERDICT: PASS

…e deployment-level pins

The test layer compiles under tsconfig.test.json: a reader's posture is the
rung union, so each fixture reader spells its rung `as const`, and the
rung-only case reads the admin grant's capability list through a non-empty
precondition rather than spreading an optional array.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwC9jjzyBACUnk1V9V
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e94fcd240ecc95c3fecb352f7010958877dade4a
Local-runs: none
Scope: packages/spec/src/security/capabilities.ts + packages/plugins/plugin-audit/src/audit-log-read-visibility.ts, .changeset/26750-ledger-deployment-level-rows.md (narrowing face, no remainder); cited code read on the live tree: packages/plugins/plugin-audit/src/parent-record-read-gate.ts, packages/plugins/plugin-audit/src/audit-writers.ts, packages/plugins/plugin-audit/src/auth-event-audit.ts, packages/plugins/plugin-audit/src/read-audit.ts, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/assemble-execution-context.ts, packages/core/src/security/posture-ladder.ts, packages/metadata-core/src/meta-write-capability.ts, packages/plugins/plugin-security/src/objects/default-permission-sets.ts, packages/plugins/plugin-security/src/security-plugin-rls-filter.ts, packages/plugins/plugin-auth/src/auth-session-audit.ts, packages/services/service-automation/src/run-resume.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-memory/src/memory-driver.ts, packages/objectql/src/engine.ts, docs/adr/0095, docs/adr/0131.

This head follows 3fa0b743 (PASS, this seat) and af925f22 (PASS, this seat). The delta 3fa0b743..e94fcd24 is one commit touching three plugin-audit test files only: audit-log-deployment-level-rows.integration.test.ts (three fixture rungs typed as const, applied under each of the three postures by describe.each), audit-log-read-visibility.integration.test.ts (one fixture rung as const), and audit-log-read-visibility.test.ts (ADMIN_FULL_ACCESS_CAPABILITIES.systemPermissions ?? [] hoisted into a local, with a new assertion that the list is non-empty, so the rung-only pin stays non-vacuous). No source, changeset, spec, docs or generated file moved: git diff --name-only 3fa0b743..e94fcd24 lists no non-test path. The net diff against origin/main is the reviewed diff plus these typing edits (11 files, +567/-119). Every judgment the delta does not touch is re-stated from the previous records and still holds against the tree.

Check-runs: on 3fa0b743, Type Check · workspace (and its TypeScript Type Check step) concluded failure; the delta is the fix the dev names for it (TS2322 on the widened posture string literals, TS2488 on the optional spread), and it is confined to the three test files. On e94fcd24 at review time: completed success: Governed Surface Queue Guard, PR-body guards, Check PR Size, Check Documentation Links, filter; skipped: the opt-in and matrix placeholders; still in_progress: Type Check · workspace (the one that matters for this delta), the other Type Check gates, every Lint gate, Check Changeset, Spec property liveness, Dogfood Regression Gate, Build Core, Build Docs, Build UI Deps, Plan Test Core shards, Live E2E UI, Flag docs, Auto Label. The verdict below is on the diff, the card and the completed runs; the in-progress runs, Type Check · workspace first among them, are the queue's gate.

① Derived judgments

  • Accept-set, the ledger read. Unchanged since af925f22. Every non-system engine read of sys_audit_log (find, findOne, count, aggregate) by a context not carrying the PLATFORM_ADMIN rung gets tenant_id not null and not empty ANDed into its where after the parent-record gate. Right: ruling A (6457448424), one rule in every posture, the holder of view_all_audit_log included.
  • Under a wall, shipped sets: no change. sys_audit_log_org already excluded NULL-tenant rows. Right. The cross-organization half of a custom walled set stays on security(plugin-audit): a sys_secret confidentiality gap, observed beside the 18.2.0 wave-2 clauses — detail withheld pending maintainer #26640 (withheld). Right, declared in the changeset.
  • Platform administrator. The rung is derived once, server-side (resolve-authz-context.ts 6d, not wall-gated) and carried by assemble-execution-context.ts; no transport reads a posture off the request. Not client-forgeable. Right.
  • Fail closed on no rung. A context built outside the resolver (a runAs:'user' automation run, run-resume.ts seeds no posture) is served organization rows only; narrower, never wider; declared in the changeset since 3fa0b743. Right.
  • Writes, system reads, context-less reads: unchanged. Right.
  • Driver semantics. $ne: null is IS NOT NULL on driver-sql and excludes null and missing on memory/mongo; $ne: '' is ANDed behind it. Right.
  • Stamping premise. Under single the session.create.before hook stamps activeOrganizationId; the residual NULL class is the ruling's own consequence, named in the changeset's Before bullet. Right.
  • Public surface. The read-visibility module is not re-exported; the spec change is one description string carried by no generated artifact. Unchanged by the delta. Right.
  • The delta itself. The as const edits change no fixture value: the integration pins still read the same three readers under single, group and isolated, and the type the fixtures now satisfy is the engine's ExecutionContext.posture (AuthzPosture), the same type the rule reads at runtime, so the pins are typed against the real contract rather than a widened string. The ?? [] hoist keeps the rung-only pin meaningful by asserting the admin capability list is non-empty before spreading it. Test-only, right.
  • Dogfood pin flip. Unchanged since af925f22; a faithful consequence of the ruling, not a fix-to-green. Right.

② Semver level

  • Clause-②: no (narrowing) and Metadata-protocol: none on the PR body and in the changeset; the changeset is byte-identical to 3fa0b743. Right.
  • patch for @objectstack/plugin-audit and @objectstack/spec, **BREAKING** banner, adr-0087: not-required (no-migration-prescription) marker, upgrade note: present. Right by AGENTS.md Post-Task Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3. Test-only edits move no published surface and need no changeset change. Check Changeset is in progress on this head; it was green on af925f22.

③ Boundary flags

  • Should-fix 1 (af925f2 record): resolved on 3fa0b743, carried unchanged here (changeset Unchanged bullet names the rung condition and the run-as case). seedRunGrants out of scope; no follow-up card cited yet (optional).
  • Should-fix 2 (af925f2 record): resolved on 3fa0b743, carried unchanged here (capabilities.ts comment names the rung beside the bypass; description byte-identical).
  • Should-fix 3 (af925f2 record): pending. The card still holds four comments, the last being the claim (6519222523); the dispatch-gates reconciliation for the final head has not landed. The gate families are unaffected by this delta (test typing only), so a reconciliation on this head or af925f22 describes the same code; it belongs on the card before the queue.
  • Should-fix 4 (3fa0b74 record): pending. The PR body's Verification section still reads "Final head af925f22"; two heads later it should name e94fcd24.
  • Red Type Check · workspace on 3fa0b743. Flagged by the dev and answered by this delta; the tree agrees the only typing hazards are the four fixture literals and the one optional spread. Its conclusion on this head is still in progress. Nothing else is open: no open_questions, no TODO/FIXME in the diff.
  • Cross-organization half: escalated to security(plugin-audit): a sys_secret confidentiality gap, observed beside the 18.2.0 wave-2 clauses — detail withheld pending maintainer #26640, withheld there. Accepted. The card's ordered measurement: reported by the dev, not re-measured here (read-only), not load-bearing for the rule.

④ Design judgment

packages/spec/src/security/capabilities.ts (hits). Byte-identical to 3fa0b743. The published row's added sentence states for single what ruling A ordered, consistent with the sentence before it, and holds against its first consumer, the plugin-audit middleware, pinned per posture in audit-log-deployment-level-rows.integration.test.ts, whose fixtures the delta types against the engine's own AuthzPosture without changing a value. The comment above the row names the rung beside the bypass. Scope 'org' unchanged. Nothing left to break.

packages/plugins/plugin-audit/src/audit-log-read-visibility.ts (the read rule, in scope on the narrowing face). Byte-identical to af925f22. The narrowing makes the spec's own refusal real where no row policy did. The rung cannot be supplied by a client; the clause is ANDed after the gate; count and aggregate ride the same AST; a mis-cased rung, a position, a permission or a capability all answer false (the unit pin now also asserts the admin capability list is non-empty, so the capability case is a real negative, not an empty spread); findOne by id of a withheld row answers null as the gate does. The no-rung case is fail-closed and declared. No must-fix.

.changeset/26750-ledger-deployment-level-rows.md (in scope on the narrowing face). Byte-identical to 3fa0b743. Declaration lines, banner, ADR-0087 marker, upgrade note and the qualified Unchanged bullet all present and matching the tree.

Must-fix: none.
Should-fix: (3, carried) the dispatch-gates reconciliation lands on the card before the queue; (4, carried) the PR body's "Final head" line names e94fcd24; (optional) cite the follow-up card for seeding the rung in seedRunGrants once one exists.

Implemented-by: claude/project-thread-edzevh
Reviewed-by: session_016WV4czVnUdQQCVKSW5J2uk

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk

Seat ACCEPT · PR #28047 · head e94fcd240ecc95c3fecb352f7010958877dade4a · 2026-10-30T11:48Z

维护者速读

  • 改了什么:合规审计账本里「部署级」的行(没有组织归属、tenant_id 为空,比如全局设置变更、用户导入、平台管理员名册)今后只给平台管理员看;组织级的审计读者只看到本组织的行。三种租户形态同一条规则。
  • 为什么改:落实你批的裁决 A([Decision] Under the single posture, does an organization-scoped audit-ledger reader also read deployment-level ledger rows, or are those for the platform administrator only? #26750,裁决 6457448424)。之前单组织部署里,被授予「看全部审计日志」的非管理员也能看到部署级配置历史。
  • 风险与回滚:收窄(BREAKING 标注):单组织部署里把该能力授给非管理员的客户,那个人会少看到部署级的行;升级说明已写。只多一个读条件,不改写入;回滚即 revert。
  • 先看:plugin-audit/src/audit-log-read-visibility.ts 的新条件,和 changeset 的升级说明。
  • 两分钟验证:用非管理员的审计员身份读账本,部署级的行不再出现,本组织的行照常;平台管理员两者都能看到。
  • 需要你做的:无(Tier S,契约复审已过,按本席落地授权落地)。

席位核对

  • Shape: draft → main, Fixes #26750. 11 files, +567/−119: the read rule (audit-log-read-visibility.ts), one description sentence + one comment clause in packages/spec/src/security/capabilities.ts (text only), one docs sentence (record-view-auditing.mdx), the changeset (BREAKING banner, adr-0087: not-required (no-migration-prescription)), plugin-audit tests, two dogfood files. check-governed-merges --pr 28047: NOT governed.
  • Declarations: Clause-②: no (narrowing), Metadata-protocol: none, Skills-docs: updated here — as amended on the claim (6519222523).
  • Contract review (narrowing face, CONTRACT_REVIEW_TIER): PASS on af925f22 (6520912495), delta PASS on 3fa0b743 (6521063979, should-fixes 1–2 folded), delta PASS on this head (6521556719, test-only as const / ?? [] fix for the red Type Check). No must-fix.
  • Measurement the ruling ordered: under group / isolated the card's custom set (ledger read + capability, no ledger row policy) does NOT reach deployment-level rows on main (the baseline policy and default row scope still apply). The new rule closes the deployment-level half in every posture. A set that authors its own ledger row policy or holds the read bypass is the already-documented out-of-reach case; it stays withheld on security(plugin-audit): a sys_secret confidentiality gap, observed beside the 18.2.0 wave-2 clauses — detail withheld pending maintainer #26640 (Acceptance notes, no detail here).
  • Pin flips (caused by the ruling, not fix-to-green): audit-log-audit-capability.dogfood.test.ts (bulk rows now stamped with the holder's organization; subject unchanged), admin-ledger-decision-metadata.dogfood.test.ts (re-armed: producer at rest + the ruling's read-side pin; armed check kept), plus two plugin-audit fixtures of the same cause. Ablations: 3, red then restored.
  • Local (card thread, report 6521529534): dispatch-gates 106/106 on af925f22 and on 3fa0b743, 0 NOT MEASURED; full dogfood green (357 files, 2877 tests); plugin-audit 1077/1077; lint and check:generated exit 0.
  • Gate check-runs on this head: 50 total, 41 success, 9 skipped, 0 failed. mergeable_state: clean.
  • Accepted deviation — PR body correction (the relay cannot re-edit a PR body): Verification names "Final head af925f22"; the final head is e94fcd24 (two commits on top: should-fixes 1–2 prose, then the test-type fix).
  • Out of scope: seedRunGrants seeds no posture rung, so a runAs: 'user' flow run as the platform administrator is served organization rows only (fail-closed) → filed automation: a runAs:'user' run carries no posture rung, so a platform administrator's flow is served organization ledger rows only #28070. content/docs/permissions/system-context.mdx "Lose:" row does not list the new clause → dropped — incomplete, not wrong (system context bypasses the read rule), no card.
  • Landing: Tier S, under the maintainer's landing grant for this seat term (2026-10-30 10:20Z, seat-1 thread). Ready + auto-merge through the relay.

ACCEPT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant