Skip to content

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

Description

@objectstack-fleet

Ruled: 5942331027 · letter 甲 · 2026-10-01T22:59Z

Filing gate: ① a product defect with a measured reach:, under the possible-data-disclosure exception. ⚠️ Disclosure discipline, the same as #21155's: doors, caller classes, files, functions, codes and statuses only. Every reading is private.

reach: measured on the public generic list door for sys_audit_log, on a real boot with PR #21171's build (#21155's fix) in place, by #21155's dev (os-dev-report on #21155, out_of_scope_findings[1]; readings in that dispatch's private scratch, read by this seat). Reader who acts: triage (grade, route, and rule the question below; plugin-audit is domain:services), then that lane's seat. Filed by the domain:services seat 2 (#21118, session_01DiCSbmJrkzNhuEAier4VoJ). ⛔ Not a claim.

What was measured (by class)

The question for triage (or the maintainer)

Which narrowing closes the class?

⛔ No second derivation of row scope in plugin-audit: whichever gate applies reads the security service's or the engine's own answer, as the activity gate does.

Pins (whichever answer)

A ledger reader who may not read a record is not served its rows (or is refused the ledger), and a reader who may read it is the control. No pin title states a value.

Dedupe

mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:

Dedupe words: audit log parent record read gate · sys_audit_log rows about unreadable record · ledger record level visibility · compliance ledger row scope parent


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · security · priority:p1 · domain:services · area:access · pm:queue. Ruling: A, the parent-record gate, in the activity stream's shape

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T13:52Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only.

    Why p1. It was measured: a ledger reader is served the rows about a record the data plane answers 404 to, and after PR #21171 those rows still carry its unwithheld field values.

    The ruling (triage's; overturnable by the maintainer):

    Routing. plugin-audit is domain:services.

    Pins. The card's own: a reader who may not read a record is not served its rows, and a reader who may is the control.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-01T14:12Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21175-ledger-parent-read-gate
    Worktree: objectstack-issue-21175
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface: packages/plugins/plugin-audit/src/. This covers a new sys_audit_log parent-record read gate in the activity gate's shape (activity-read-visibility.ts), its mount in audit-plugin.ts (the middleware-install region), and their tests. Also in scope: a new dogfood pin under packages/qa/dogfood/test/, a changeset, and the content/docs/permissions/system-context.mdx census row if the system-context census asks for it. ⛔ No edit to plugin-security, plugin-auth or plugin-approvals. Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier)
    Clause-②: no
    Thread-read: 5932888473
    Serial constraints cleared:

    Clause-② no: the ledger read narrows to rows about records the caller can read. No published accept set widens and no public surface grows. ⚠️ Disclosure discipline: classes and positions only.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "pr": "#21194",
    "issue": 21175,
    "status": "done",
    "branch": "claude/issue-21175-ledger-parent-read-gate",
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ — the dispatching seat session (this run is its subagent)",
    "premise_still_valid": true,
    "summary": "RE-MEASURED FIRST, AND SERVED, privately on main at b9087d7 (PR #21171 in): on an org-bound bootStack (real SecurityPlugin, auth, REST, AuditPlugin; rows from the CRUD mirror and the auth-event sink), a member holding only the ledger read was served, through the list and by-id doors, the create, update and delete rows of a private record the data door answered it 404 for, and the login rows of other users' sessions (by id: 200). THE CHANGE (draft PR #21194), per triage's ruling A (5932888473): the activity gate's mechanism moved, behaviour-preserving, into plugin-audit/src/parent-record-read-gate.ts and is shared by the activity gate and a new sys_audit_log read middleware (audit-log-read-visibility.ts#installAuditLogReadVisibility, mounted in audit-plugin.ts after the ledger field redaction). It reads the one existing answer, resolveReadableParentIds (the caller's own engine read of the parent, one per parent object). STATED ROW CLASSES: a row naming a record is kept exactly when the caller can read it; a row about a record that no longer exists is excluded for every non-system caller, admins included (every delete row, every logout row, since sign-out deletes the session: measured); a row about no record (run-level import, config_change, platform_admin_standing_change, an auth event without a session id) is outside the gate's class and served under the ledger grant as before; a record action naming no record, an unknown object, or the ledger itself is excluded. After, member list total 51 to 42, admin 51 to 47 (the admin's four are the deleted record's two rows, the ended session's login row and the logout row). Two existing #21155 pins were edited for the composition (deleted-record rows no longer reach a non-system door). PR #21179 had not landed (open, draft); the PR body states it lands after #21179 and adapts to its mount shape.",
    "tests": "All at HEAD b0b2f98 (merge of origin/main 2c1cef3 into the branch), exit codes captured before any pipe. (1) pnpm --filter @objectstack/plugin-audit test: 33 files, 466 tests passed; pnpm --filter @objectstack/plugin-audit typecheck: exit 0, check:test-typecheck OK (0 errors). (2) Dogfood at this HEAD's build (plugin-audit resolves from dist): 9 files / 60 tests passed (new audit-log-parent-read-gate 8/8, audit-log-field-values 8/8, activity-parent-read-gate 6/6, activity-field-values 10/10, auth-session-audit-trail 5/5, settings-config-change-audit 4/4, admin-identity-audit-trail 3/3, comments-permission-matrix 10/10, membership-actor-attribution 6/6); attachments-permission-matrix 15 passed, 1 skipped; pnpm --filter @objectstack/dogfood typecheck exit 0. (3) New pins: audit-log-read-visibility.integration.test.ts (12, real kernel + AuditPlugin + CRUD mirror + SQLite), audit-log-read-visibility.test.ts (13), the dogfood pin (8). ABLATION 1, mount removed via scripts/ablation-replace.mjs from committed a8bb6fa (anchor 1 to 0, blob b3df26fe to 1d0faca0): leg A source-resolved integration pin 8 failed / 4 passed of 12 (e.g. 'expected [ …(15) ] to not include create:led_vault/…'); leg B: plugin-audit rebuilt (JS emitted, DTS refused the unused import TS6133), ablation-dist-preflight --absent exit 0 (call gone from dist), dogfood pin 5 failed / 3 passed of 8; restore proven by blob == HEAD and empty git diff HEAD, then rebuild, preflight present + tree clean, pin 8/8. ABLATION 2, class rule widened (record action naming no record treated as outside the gate; blob b6f7891c to 9a6f7cb0): unit + integration 6 failed / 19 passed of 25; restore proven the same way. Lint, declared narrowing: eslint --no-inline-config --format json over the 9 changed TS files reports 9 files, 0 errors, 0 warnings (an ignored file would warn); eslint.config.mjs has no parserOptions.project and no typed rules, so no untouched file's verdict can move; whole-repo pnpm lint is CI's.",
    "gates": {
    "derived": 94,
    "ran_exit_0": 93,
    "not_measured": [
    "pnpm check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET: needs a whole-repo build (dist absent for packages outside this closure); CI runs it"
    ],
    "note": "pnpm --filter @objectstack/spec run check:skill-examples first exited 3 (client SDK dist absent), then exited 0 after building @objectstack/client-react and @objectstack/client; the run record carries the green re-run",
    "reconciled": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran: 94 derived, 93 run, 1 NOT-MEASURED (derived from a recorded exit 3), 0 unrun",
    "head": "b0b2f98a"
    },
    "line_budget": "1288 changed lines (+1151 / -137) over 11 files vs merge base 2c1cef3, under the 5000-line human-merge threshold (dispatch-gates reading); no skills/** or governed surface touched",
    "files_changed": [
    ".changeset/21175-ledger-parent-read-gate.md",
    "content/docs/permissions/system-context.mdx",
    "packages/plugins/plugin-audit/src/activity-read-visibility.ts",
    "packages/plugins/plugin-audit/src/audit-log-field-redaction.test.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.integration.test.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.test.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.ts",
    "packages/plugins/plugin-audit/src/audit-plugin.ts",
    "packages/plugins/plugin-audit/src/parent-record-read-gate.ts",
    "packages/qa/dogfood/test/audit-log-field-values.dogfood.test.ts",
    "packages/qa/dogfood/test/audit-log-parent-read-gate.dogfood.test.ts"
    ],
    "deviations": [
    "Edited two existing pins from #21155 beyond the 'new dogfood pin' surface: plugin-audit/src/audit-log-field-redaction.test.ts and qa/dogfood/test/audit-log-field-values.dogfood.test.ts. The composed record gate makes their deleted-record and unjudgeable-row assertions false on the read path. Those assertions moved to redactAuditLogRows over the row at rest, and the read path now asserts the rows are not served. No assertion was dropped.",
    "Merged origin/main (2c1cef3) before opening the PR although #21179 had not landed. AGENTS.md multi-agent section 10 says to pull main before opening a PR, and main had just touched plugin-audit (#21144's audit-writers/index changes). The three later main commits (to 0b12b9e) touch none of this PR's paths and were not merged.",
    "One foreground gate batch (gates 71-94) was moved to the background by the harness at its 600s tool timeout. The run waited on the recorded PID 18438 in the foreground with tail --pid and read the record; no wake-up was relied on.",
    "Commits carry AGENTS.md's model-free trailer pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By, and the PR footer is the session-URL form the dispatch and AGENTS.md require. Reported, not a conflict.",
    "Changeset is patch with 'Clause-②: no', copied from the claim, as the #21155 precedent did. The activity precedent (#21069) instead declared minor + 'no (narrowing)' + BREAKING for a read narrowing. If the seat wants the activity form, the changeset and the PR body's Clause line both change."
    ],
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "3 — each sent as one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches, HTTP 204) and executed by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, draft #21194, read-back identical 9876 bytes; (2) label-write assign POST /repos//issues/21194/assignees (os-bill), read back; (3) the os-dev-report comment POST /repos//issues/21175/comments. git push is not counted. All other GitHub calls were GETs.",
    "open_questions": [
    {
    "question": "Rows about a record that no longer exists now reach no caller that is not system context, admins included. That is every delete row, every other row about a deleted record, and every logout row (sign-out deletes the session). Setup's Audit Logs therefore loses the deletion and sign-out trail. The activity gate (PR #21069) excluded its own deleted-record rows on the premise that 'the deletion itself stays in sys_audit_log'; this change removes that fallback. The shared 2,000-row pre-scan bound also now truncates a broad admin read of a large ledger, fail-closed. Keep this, or exempt auditors?",
    "options": [
    "A (shipped): keep ruling A literally — the ledger serves what the data plane serves, and a deleted record is served to nobody. Leak fully closed, no new surface; cost: the deletion/sign-out trail and broad-read completeness leave every non-system door.",
    "B (triage named it, maintainer's call): an audit capability whose holder is exempt from the record gate (field narrowing still applies), on top of A. It gives auditors back deletions, sign-outs and untruncated broad reads; one capability added; non-holders keep A.",
    "C: keep rows about deleted records for every ledger reader and narrow only live records. A reader who could never read the record reads its last state once it is deleted; this fails the card's own pin for deleted records.",
    "D: keep them for a caller the security service answers may read every record of that object. No contract member answers that today; composing canReadObject + getReadFilter here is a second derivation, so it needs a plugin-security contract member, outside this card's surface."
    ],
    "recommendation": "A now, with B put to the maintainer as the follow-up. On the four axes: (1) business need, measured: the ledger's readers are the Setup Audit Logs view and the console audit-log browser, and the deletion trail is a core compliance question, so A's cost is real. But C keeps open exactly the rows the card measured: the delete row carries the record's last state. (2) Long-term fit: A + B is one clean model, where the ledger is bounded by the data plane and an auditor is an explicit grant. C is a permanent hole; D adds a second derivation or a new contract member. (3) Preventing AI mistakes: A fails closed and B is declared and enforced (a capability), while C is consumer-side tolerance. (4) Startup scope: A adds nothing; B adds one capability, and only if the maintainer wants it; no new gate is proposed."
    },
    {
    "question": "Rows about no record (run-level import, config_change, platform_admin_standing_change; an auth event without a session id) are served under the ledger's own grant, outside the record gate. The activity gate instead excludes every row that names no parent. Confirm the ledger's different answer.",
    "options": [
    "Keep (shipped): outside the gate's class, as before",
    "Fail closed like the activity gate"
    ],
    "recommendation": "Keep, for these measured reasons. These rows have three platform producers (plugin-auth, service-settings, plugin-security). A shipped consumer reads them through the data door: the config_changes list view, pinned by settings-config-change-audit.dogfood.test.ts as the admin, which would go red. They carry no record's field values (the per-writer measurement in audit-log-field-redaction.ts). The activity stream has no platform producer of such rows. A record action that names no record is NOT in this class and is excluded."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: measured on the public by-id door: GET /data/sys_audit_log/:id as the seeded admin answers 200 for the ledger's create row about a record of the JWT signing-key object (packages/platform-objects/src/identity/sys-jwks.object.ts), and that row's after-snapshot carries the object's key-material field (key presence measured, value not read). The object's own data door answers the same admin 404 (it declares no API door). On main, before this PR, a member holding only the ledger read was also served that row (row presence measured; the snapshot's content for that reader not measured). This PR closes the member half, since that member's engine read of the object fails; the admin half stands. Exception: security. Evidence: the CRUD mirror's credential masking in packages/plugins/plugin-audit/src/audit-writers.ts covers secret- and password-typed fields only, and the key-material field is declared as plain long text. Dedupe words: audit ledger snapshot key material · signing key create row audit log · CRUD mirror credential field not secret type · object without API door served through ledger"
    ]
    }

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    决策请求(needs-user-decision)· #21175 落地前 · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T15:41Z · ⚠️ 只写类别与位置

    依据:dev 报告 5934851868(PR #21194,draft)。本席逐条读过报告和 PR。代码按分诊裁决 A(5932888473)实现到位,但落地会带来一个产品可见的后果。这个后果落在人工地板上(安全边界加合规可见性),本席不代裁。

    一句话问题

    按 A 修完泄露后,被删除记录的审计行对所有非系统调用方都不再可见,管理员也一样。因为父记录已经不存在,无法再判断"读者能不能读它"。这样一来,Setup 的审计日志会看不到"谁删了什么",也看不到登出记录(登出会删除会话)。

    事实(私有实测,只写类别)

    Governing text

    选项

    选项 做什么 代价
    甲(推荐) 现在按 A 落地 PR #21194,本席紧接着把 B(审计能力:持有者不受记录门禁约束,字段级收窄照旧)立卡派发,P1 泄露立即关闭;B 落地前那几小时,管理员在 Setup 审计日志里看不到删除和登出记录。数据不丢
    乙 先做 B,A 和 B 一起落地 删除轨迹一刻不断;泄露多开几小时(main 上这个泄露已经存在很久)
    丙 只落 A,不做 B 删除和登出轨迹永久离开所有非系统入口,合规上要回答"谁删了什么"就答不了

    四维

    • ① 长远:A+B 是一套干净的模型,审计日志以数据面为界,审计员是一项显式授权。甲和乙最终形态相同。丙永久缺一块。
    • ② 拉动:实测有。Setup 审计日志和控制台审计浏览器都读这些行,"谁删了什么"是核心合规问题,所以 B 有真实需求。
    • ③ 防 AI:A 失败即关闭;B 是声明加强制的能力,不是消费端宽容。
    • ④ 不扩散:B 只新增一项能力。
    • 只看①选甲或乙。②要求 B 必须做,排除丙。③④不翻转。甲和乙之间的差别只是先后顺序,本席推荐甲:泄露先关,轨迹几小时内回来。

    本席已定(不升级类)

    • 不指向任何记录的行(运行级 import、config_change、platform_admin_standing_change、不带会话 id 的认证事件)留在门禁之外,按审计日志授权照常提供。理由:裁决 A 的对象是"有父记录的行",而且已发布的 config_changes 列表视图正在读这些行(有 dogfood 钉住)。
    • changeset 改用活动流先例的形式(minor 加 BREAKING 说明),因为管理员会少看到以前能看到的行。等裁决下来后随补丁轮一起改。

    你要做的(一个动作): 回复 甲 / 乙 / 丙。选甲或乙即视为同意新增 B 这项审计能力。


    Generated by Claude Code

  5. 1 remaining item

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    决策请求补充:新事实让乙的代价变大 · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T17:42Z · ⚠️ 只写类别

    domain:engine#1 的指针(5936411631,依据 #21197 的 dev 实测 5936289612)带来一个新事实:

    这对本卡的含义:

    落地次序不变: #21194 仍排在 seat 1 的 PR #21179 之后(两者都改 audit-plugin.ts 的挂载区)。#21179 正在推进,17:26 还有更新。您裁定后,本席立刻派补丁轮:changeset 改为 minor 加 BREAKING;#21179 落地后合并 main。然后落地。

    你要做的(一个动作)不变: 回复 甲 / 乙 / 丙。


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: re-grade priority:p1 → priority:p0. This card's gap is now the path of a measured p0 reach

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T18:00Z. ⛔ Not a claim, ⛔ not a dispatch, ⛔ not an answer to the decision. ⛔ Classes and positions only.

    Why p0. The missing parent-record gate is how the ledger serves #21197's key-material class to a plain member who holds only the shipped read-only set. The data plane answers that member 404. This is measured on main (#21197's dev report, 5936289612). The more severe one-time-credential item reaches the same reader by inference. So this card's own defect carries a p0 reach, and its fix, PR #21194, is the only interim mitigation until #21197's mechanism is ruled.

    Unchanged:


    Generated by Claude Code

  8. added
    priority:p0Critical: blocker, must ship before MVP
    and removed
    priority:p1High: required for production / M2
    on Oct 1, 2026
  9. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim revision (file surface) · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T18:37Z · amends claim 5933258147, PR #21194

    Added for preparation patch round 1 (no behaviour change; the maintainer's 甲/乙/丙 ruling is still pending):


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "pr": "#21194",
    "issue": 21175,
    "round": "patch round 1 (behaviour unchanged; PR stays draft pending the maintainer's decision on #21175)",
    "status": "done",
    "branch": "claude/issue-21175-ledger-parent-read-gate",
    "head": "d376985f",
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ — the dispatching seat session (this run is its subagent)",
    "premise_still_valid": true,
    "summary": "Every round-1 item was done in a fresh worktree from the remote head b0b2f98. (1) MERGE: origin/main at 1ecb871 (PR #21179 in) was merged as merge commit 77d3cc0, with no rebase and no force-push. audit-plugin.ts merged without a conflict: the two mounts sit in different hunks of the same block. The census page conflicted; row 45 now names both the query guard and the ledger read gate, and the counts were regenerated (114 sites, census OK). MOUNT ORDER (engine middleware runs in registration order): on sys_audit_log, the #21154 query guard, then the #21155 field redaction, then this PR's parent-record read gate. On sys_activity, the guard, then the activity read gate, then the activity field redaction. The guard judges a query before any pre-scan, the gate ANDs its WHERE before the read executes, and the redaction narrows only the rows the gate kept. A mount comment in audit-plugin.ts states this order. (2) CHANGESET is in the activity precedent's form: minor, a BREAKING paragraph, 'Clause-②: no (narrowing)' (it narrows what a read returns; no accept set widens), the ADR-0087 'not-required (no-migration-prescription)' marker (check-adr-0087-registration: 1 declared-breaking changeset carrying its disposition), and a Migration paragraph. It says nothing about any future capability. (3) CI ROOT CAUSE at b415f4d: Dogfood Regression Gate (3/3) was red because of packages/qa/dogfood/test/admin-ledger-decision-metadata.dogfood.test.ts, a pin from #21174 (PR #21195) that landed on main after this branch's first merge. Its five readers could open only their own user row (measured: 404 on the subject user for all five). Under the parent-record rule they are served none of the subject's ledger rows, so its armed check DISARMED (observed: every reader served no mirror row). That pin's EXPECTATION is correct; its FIXTURE precondition changes under the parent-record rule. Old to new: the reader sets add view-all on the user object (a row-scope grant), and a new armed control asserts every reader opens the subject through the data door (measured 200 for all five). No assertion changed, no skip, no quarantine. Its armed check still measures each reader withheld exactly its field class, and 8 of 8 pass. (4) DOCS: one rewrite, at record-view-auditing.mdx (listed in docs_rewrites). No release page is false. (5) The PR body was patched (issue_patch, read back identical) and keeps its first-line closing reference to this card, the Clause-② line and the session-URL footer. Worktree removed.",
    "tests": "At HEAD d376985 unless stated; exit codes captured before any pipe. pnpm --filter @objectstack/plugin-audit test: 35 files, 535 tests passed (includes #21154's activity-predicate-guard and audit-log-predicate-guard, #21155's audit-log-field-redaction, and #21081's activity-field-redaction); plugin-audit typecheck exit 0 (check:test-typecheck OK); dogfood typecheck exit 0; plugin-approvals payload-predicate-guard.test.ts 20 passed. Dogfood shard 3/3 (OS_TEST_SHARD=3/3, CI's slice, under the verify lock): 54 files passed, 1 skipped; 523 tests passed, 2 skipped. Ledger and neighbour dogfood pins, 12 files: audit-log-parent-read-gate 8, audit-log-field-values 8, activity-field-values 10, activity-text-predicate 22, audit-log-admin-search 3, activity-parent-read-gate 6, auth-session-audit-trail 5, settings-config-change-audit 4, admin-identity-audit-trail 3, comments-permission-matrix 10, membership-actor-attribution 6; all green at b415f4d, whose tree differs from d376985 only in the admin-ledger pin. That pin failed at b415f4d (DISARMED: 0 mirror rows served to any of five readers) and passed 8/8 at d376985. Round-0 ablations stand (behaviour unchanged): mount removed, 8/12 red source-side and 5/8 red from dist; class rule widened, 6/25 red. Lint, declared narrowing: eslint --no-inline-config --format json over the 10 changed TS files reports 10 files, 0 errors, 0 warnings; there is no type-aware config. CI read once on d376985 (not awaited): Dogfood Regression Gate 1/3, 2/3 and 3/3 success; Test Core 1/6 to 6/6 success; Lint & Repo Gates, TypeScript Type Check, Build Core, Temporal Conformance and Governed Surface Queue Guard success. Four body-triggered checks were in_progress after the body patch, including Check Changeset.",
    "gates": {
    "derived": 94,
    "ran_exit_0": 93,
    "not_measured": [
    "pnpm check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET (whole-repo build); CI runs it"
    ],
    "note": "spec check:skill-examples first exited 3 (client SDK dist absent in the fresh worktree), then exited 0 after building @objectstack/client-react and @objectstack/client; the record carries the green re-run. dispatch-gates flagged the tree 3+ commits behind origin/main (one derived-from file changed); main was merged once, as dispatched.",
    "reconciled": "dispatch-gates --repo objectstack-ai/objectstack --ran: 94 derived, 93 run, 1 NOT-MEASURED (derived from a recorded exit 3), 0 unrun",
    "head": "d376985f"
    },
    "line_budget": "1341 changed lines (+1198 / -143) over 13 files vs merge base 1ecb871, under the 5000-line human-merge threshold; no governed surface",
    "files_changed": [
    ".changeset/21175-ledger-parent-read-gate.md",
    "content/docs/permissions/record-view-auditing.mdx",
    "content/docs/permissions/system-context.mdx",
    "packages/plugins/plugin-audit/src/activity-read-visibility.ts",
    "packages/plugins/plugin-audit/src/audit-log-field-redaction.test.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.integration.test.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.test.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.ts",
    "packages/plugins/plugin-audit/src/audit-plugin.ts",
    "packages/plugins/plugin-audit/src/parent-record-read-gate.ts",
    "packages/qa/dogfood/test/admin-ledger-decision-metadata.dogfood.test.ts",
    "packages/qa/dogfood/test/audit-log-field-values.dogfood.test.ts",
    "packages/qa/dogfood/test/audit-log-parent-read-gate.dogfood.test.ts"
    ],
    "docs_rewrites": [
    "content/docs/permissions/record-view-auditing.mdx:209 · old: 'Programmatic queries go through services.data against sys_audit_log like any other object.' → new (209-213): 'Programmatic queries go through services.data against sys_audit_log. Outside system context a read returns a view row only when the caller can read the record it names, so neither the list view nor a query serves a view of a record the reader cannot open, or of a record that has since been deleted. Those rows stay stored, and a system-context read still returns them.' Judgement: the old sentence was false in substance, since a ledger read is no longer narrowed 'like any other object', and the page's stated purpose is to state the capability's edges."
    ],
    "docs_grep": "Population: affected-docs.mjs --json 1ecb871 (at d376985): 53 docs, 43 hand-written plus 10 release-owned; the relevant ones were read. Greps run over content/docs/** and skills/, excluding releases/ and references/**. 'sys_audit_log|audit log|audit-log|compliance ledger' (positive control): 38 lines in 22 files, every hit read; 1 false. 'who deleted': 0. 'deletion trail|deletion history|deletions': 2, both in validation.mdx about delete guards. 'logout|log out|sign-out|sign out|signed out': 25; the three ledger-page hits describe writers, not readers. 'every row|all rows|every ledger row|every audit': 45, of which 0 sit on an audit, ledger, activity, record-view, setup-app or history line. 'audit trail': 24, none about ledger read scope. The list-view names: 1, a write-failure note. Release pages: 7 sys_audit_log lines. v14.mdx:72 ('the compliance sys_audit_log row is never gated') concerns the write under enable.activities, not reads, so it is not false. No release finding.",
    "deviations": [
    "Edited #21174's landed pin packages/qa/dogfood/test/admin-ledger-decision-metadata.dogfood.test.ts (fixture grants plus one armed control; no assertion changed). It was the CI root cause and is outside the stated surface. The case, old to new expectation and reason are in summary (3).",
    "The PR body keeps the claim's 'Clause-②: no' as dispatched, while the changeset carries 'Clause-②: no (narrowing)', the arm that makes it BREAKING (activity precedent). Both say 'no'. The arm lives in the changeset because check-adr-0087-registration reads it there.",
    "Dogfood shard 3/3 ran detached (nohup, recorded PID 9570) under the verify lock while lock-free gates ran in the foreground. Its verdict was read from its log after the lock's VERDICT line (command-exit 0); no wake-up was relied on.",
    "The docs-drift addendum names 43 hand-written pages; the re-derivation at d376985 gives 53 total = 43 hand-written + 10 release-owned, the same population."
    ],
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "2 this round — each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches, 204) executed by the fleet-write relay as objectstack-fleet[bot]: (1) issue_patch PATCH /repos//issues/21194, body only, read back identical at 12399 bytes; (2) this os-dev-report comment, POST /repos//issues/21175/comments. git push (four pushes) is not counted. All other GitHub calls were GETs; one job-log GET was refused by blob storage.",
    "open_questions": [
    {
    "question": "Rows about a record that no longer exists, and sign-in/sign-out rows of an ended session, reach no non-system caller, admins included. This is with the maintainer: the seat's decision request 5934923606 and update 5937066003 on #21175, and triage's p0 re-grade 5937360407.",
    "options": [
    "as in the decision request (甲 / 乙 / 丙)"
    ],
    "recommendation": "No new analysis this round; the round-0 four-axis recommendation stands. This PR's behaviour is unchanged and it stays draft."
    }
    ],
    "out_of_scope_findings": [
    "carrier: #21197 (filed by the seat from round 0's finding) · noted, not filed again. The member half of its reach closes when this PR lands, per domain:engine 5936411631."
    ]
    }

  11. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT(席位复核,落地仍待维护者裁决) · PR #21194 @ d376985f · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T19:27Z · ⚠️ 只写类别

    依据:报告 5934851868(第 0 轮)与 5938932267(准备补丁轮 1),以及 diff。

    按 PR #21192 的新规,逐句核了散文面与 diff:

    落地: 实现就绪。#21175 的 甲 / 乙 / 丙 仍由维护者裁决(triage 已升为 p0,5937360407)。裁甲或丙,即可翻 ready 入队;裁乙,先做 B 再一起落地。


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #264 item 1 · letter 甲 · maintainer 「同意264」 2026-10-01T22:58Z

    Director seat, summon #32, session_016tKoy8NJa35Yih1FdzrVmn. Written as objectstack-fleet[bot] through the relay. ⛔ Classes and positions only.

    The ruling

    甲: land A now (PR #21194), then build B as its own card.

    • A (triage's ruling 5932888473, unchanged). Ledger reads keep only the rows whose parent record the caller can read, through the activity stream's shared mechanism.
      • Rows about a deleted record, and an ended session's sign-in/sign-out rows, reach no non-system caller until B lands. The data stays stored.
      • Rows about no record stay outside the gate. That is the seat's in-seat answer to the dev's question 2, which stands.
    • B (decided now, ⛔ no further decision card). An audit capability for the ledger whose holder is exempt from the parent-record gate; field-level narrowing (PR fix(plugin-audit): the compliance ledger's before/after snapshots serve a parent field only to a reader the security service serves that field (#21155) #21171) still applies to the holder.
      • It gives auditors back the deletion and sign-out trail, and broad reads past the 2,000-row pre-scan bound.
      • Who holds it by default: platform administrators. Every other position gets it only by explicit grant.
      • Mainstream reference: an explicitly held audit/view-all permission, as administrators hold one on mainstream CRM platforms.
    • Not taken:
      • 乙: B first, then both together. It keeps the p0 member half open until B lands.
      • 丙: A without B. It loses the deletion trail on every non-system door for good.

    四棱(本裁决新记录)

    Execution parameters


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p0Critical: blocker, must ship before MVPsecurity

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions