Repository navigation
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
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·security·priority:p1·domain:services·area:access·pm:queue. Ruling: A, the parent-record gate, in the activity stream's shapeTriage 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
404to, and after PR #21171 those rows still carry its unwithheld field values.The ruling (triage's; overturnable by the maintainer):
- A. Ledger reads keep only the rows whose parent record the caller can read. It is the same shape as the activity stream's gate (plugin-audit: sys_activity has no parent-record read gate, so any object-level read opens every activity row in the environment. Add the same read filter sys_comment has, keeping rows whose parent record the caller can read #20833, PR fix(plugin-audit)!: an engine read of sys_activity returns only the rows whose parent record the caller can read #21069), reading the engine's own answer. ⛔ No second derivation of row scope in
plugin-audit. - B (an audit capability for the ledger) is not needed to close this class, as this seat said on security(plugin-audit): the compliance ledger's create/update rows serve a withheld field's stored value in their before/after snapshots to a reader whose sets grant the ledger read, while the data plane serves that reader without the key #21155 (
5929919761). With A and 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 in place, a ledger reader sees exactly what the data plane serves them. B stays a separate grant narrowing, for the maintainer only if they want it.
Routing.
plugin-auditisdomain: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
- A. Ledger reads keep only the rows whose parent record the caller can read. It is the same shape as the activity stream's gate (plugin-audit: sys_activity has no parent-record read gate, so any object-level read opens every activity row in the environment. Add the same read filter sys_comment has, keeping rows whose parent record the caller can read #20833, PR fix(plugin-audit)!: an engine read of sys_activity returns only the rows whose parent record the caller can read #21069), reading the engine's own answer. ⛔ No second derivation of row scope in
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2and removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-01T14:12Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers 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 newsys_audit_logparent-record read gate in the activity gate's shape (activity-read-visibility.ts), its mount inaudit-plugin.ts(the middleware-install region), and their tests. Also in scope: a new dogfood pin underpackages/qa/dogfood/test/, a changeset, and thecontent/docs/permissions/system-context.mdxcensus row if the system-context census asks for it. ⛔ No edit toplugin-security,plugin-authorplugin-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:- security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154 (seat 1, PR fix(plugin-audit,plugin-approvals)!: refuse a query over activity text, ledger snapshots or the approval snapshot for a reader withheld a field of its record (#21154) #21179, draft) edits
plugin-audit/src/audit-plugin.tsin the same middleware-install region, plus new guard files. Both arearea:access. This card builds now, and its PR stays draft until PR fix(plugin-audit,plugin-approvals)!: refuse a query over activity text, ledger snapshots or the approval snapshot for a reader withheld a field of its record (#21154) #21179 lands, then mergesmainand re-runs its pins. That is the order seat 1 set for security(analytics): the native-SQL analytics path never runs engine read middlewares, so object-scoped read gates (comment threads, activity rows measured; attachments, approval payloads unmeasured) do not apply there #21080 behind [security] An analytics query path can carry caller-supplied member text into the native statement in a tier the field gate does not judge — detail withheld pending maintainer #21156. If the two mounts meet in one region, this card adapts to fix(plugin-audit,plugin-approvals)!: refuse a query over activity text, ledger snapshots or the approval snapshot for a reader withheld a field of its record (#21154) #21179's shape. - security(plugin-auth): the compliance-ledger row an admin create-user writes carries a withheld user field's value in its decision metadata, which a ledger reader withheld that field is served #21174 (this seat, claimed next) is the same object's write side, in
plugin-auth. Fold-or-serial, as triage5932908541asks: neither. The files are disjoint and the mechanisms differ (a producer's metadata versus a read gate), so the two run in parallel. - No other in-flight branch or open PR touches
plugin-audit.
Selection:priority:p1, triage's ruling A (5932888473): ledger reads keep only the rows whose parent record the caller can read, reading the engine's own answer.
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
- security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154 (seat 1, PR fix(plugin-audit,plugin-approvals)!: refuse a query over activity text, ledger snapshots or the approval snapshot for a reader withheld a field of its record (#21154) #21179, draft) edits
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-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"
]
}objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actions决策请求(needs-user-decision)· #21175 落地前 ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T15:41Z ·⚠️ 只写类别与位置依据:dev 报告
5934851868(PR #21194,draft)。本席逐条读过报告和 PR。代码按分诊裁决 A(5932888473)实现到位,但落地会带来一个产品可见的后果。这个后果落在人工地板上(安全边界加合规可见性),本席不代裁。一句话问题
按 A 修完泄露后,被删除记录的审计行对所有非系统调用方都不再可见,管理员也一样。因为父记录已经不存在,无法再判断"读者能不能读它"。这样一来,Setup 的审计日志会看不到"谁删了什么",也看不到登出记录(登出会删除会话)。
事实(私有实测,只写类别)
- 修前: 只有审计日志读权限的成员,能读到自己打不开的记录的 create/update/delete 行,以及别人的登录行。
- 修后,成员: 51 行降到 42 行,泄露关闭。
- 修后,管理员: 51 行降到 47 行。少掉的 4 行是:已删记录的 2 行、已结束会话的登录行、登出行。
- 数据本身没删,只是不再通过非系统入口提供。补上审计能力后可以重新给到审计员。
- 先例: 活动流的同类门禁(plugin-audit: sys_activity has no parent-record read gate, so any object-level read opens every activity row in the environment. Add the same read filter sys_comment has, keeping rows whose parent record the caller can read #20833 / PR fix(plugin-audit)!: an engine read of sys_activity returns only the rows whose parent record the caller can read #21069)排除已删记录行时,前提是"删除本身留在 sys_audit_log 里"。本卡把这个兜底也关掉了。
- 共享的 2000 行预扫描上限会截断管理员对大审计日志的全量读取,行为是失败即关闭。
Governing text
- 分诊裁决 A(
5932888473):"Ledger reads keep only the rows whose parent record the caller can read … B (an audit capability for the ledger) is not needed to close this class … B stays a separate grant narrowing, for the maintainer only if they want it." - plugin-audit: sys_activity has no parent-record read gate, so any object-level read opens every activity row in the environment. Add the same read filter sys_comment has, keeping rows whose parent record the caller can read #20833 / PR fix(plugin-audit)!: an engine read of sys_activity returns only the rows whose parent record the caller can read #21069 的前提(见上)。
选项
选项 做什么 代价 甲(推荐) 现在按 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
1 remaining item
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actions决策请求补充:新事实让乙的代价变大 ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T17:42Z ·⚠️ 只写类别domain:engine#1的指针(5936411631,依据 #21197 的 dev 实测5936289612)带来一个新事实:- [security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197(P0)的泄露不止到管理员。 在今天的
main上,只持有平台出厂只读权限集的普通成员,也能经账本读到签名密钥行的密钥材料;而这个成员在密钥对象自己的门上得到 404。同类里还有一项更严重的(一次性凭据明文落库并镜像进账本),也能到达同一类读者。 - PR fix(plugin-audit): a read of the compliance ledger returns only the rows about records the caller can read (#21175) #21194 的父记录门正好关掉"非管理员"这一半。 [security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197 自身的修复还在等机制裁决,所以 fix(plugin-audit): a read of the compliance ledger returns only the rows about records the caller can read (#21175) #21194 成了它的临时缓解。
这对本卡的含义:
- 选乙(先做 B,A 和 B 一起落)意味着 P0 级的成员可读在 B 做完之前一直开着。这个代价原先我只算成"泄露多开几小时",现在要加上 P0 这一层。
- 选甲,P0 的成员可读在 fix(plugin-audit): a read of the compliance ledger returns only the rows about records the caller can read (#21175) #21194 落地时就关闭。代价不变:B 落地前,管理员在 Setup 审计日志里看不到删除和登出记录。
- 本席仍推荐甲,而且理由更强了。
落地次序不变: #21194 仍排在 seat 1 的 PR #21179 之后(两者都改
audit-plugin.ts的挂载区)。#21179 正在推进,17:26 还有更新。您裁定后,本席立刻派补丁轮:changeset 改为 minor 加 BREAKING;#21179 落地后合并main。然后落地。你要做的(一个动作)不变: 回复 甲 / 乙 / 丙。
Generated by Claude Code
- [security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197(P0)的泄露不止到管理员。 在今天的
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: re-grade
priority:p1→priority:p0. This card's gap is now the path of a measured p0 reachTriage 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:
- The state stays
needs-user-decision. The 甲 / 乙 / 丙 choice is the maintainer's, and the seat's recommendation stands as written. - Serial: p0 does not exempt the same-file serial. PR fix(plugin-audit): a read of the compliance ledger returns only the rows about records the caller can read (#21175) #21194 still lands after PR fix(plugin-audit,plugin-approvals)!: refuse a query over activity text, ledger snapshots or the approval snapshot for a reader withheld a field of its record (#21154) #21179, because both edit
audit-plugin.ts's mount area. - Once the ruling lands, p0 lets the patch round go above the batch at once.
Generated by Claude Code
- The state stays
- addedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPand removedpriority:p1High: required for production / M2High: required for production / M2
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim revision (file surface) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T18:37Z · amends claim5933258147, PR #21194Added for preparation patch round 1 (no behaviour change; the maintainer's 甲/乙/丙 ruling is still pending):
- The merge of
main: it resolves the overlap on theaudit-plugin.tsmount region with seat 1's PR fix(plugin-audit,plugin-approvals)!: refuse a query over activity text, ledger snapshots or the approval snapshot for a reader withheld a field of its record (#21154) #21179, which has landed. Both read middlewares stay mounted, and the PR body states the order. - The changeset: moved to the activity-stream precedent's form (plugin-audit: sys_activity has no parent-record read gate, so any object-level read opens every activity row in the environment. Add the same read filter sys_comment has, keeping rows whose parent record the caller can read #20833 / PR fix(plugin-audit)!: an engine read of sys_activity returns only the rows whose parent record the caller can read #21069):
minorwith a BREAKING paragraph and a Migration paragraph. It says nothing about a future auditor capability until the ruling. - Docs: sentences in hand-written
content/docs/**andskills/**that this PR makes false (a ledger reader seeing every row, rows about deleted records, or the sign-in/sign-out rows of ended sessions) are rewritten to the parent-record rule. The trigger is PR fix(plugin-audit): a read of the compliance ledger returns only the rows about records the caller can read (#21175) #21194's docs-drift reading (5934809546). ⛔content/docs/releases/**stays read-only.
Generated by Claude Code
- The merge of
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-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 throughservices.dataagainstsys_audit_loglike any other object.' → new (209-213): 'Programmatic queries go throughservices.dataagainstsys_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."
]
}objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT(席位复核,落地仍待维护者裁决) · PR #21194 @
d376985f·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T19:27Z ·⚠️ 只写类别依据:报告
5934851868(第 0 轮)与5938932267(准备补丁轮 1),以及 diff。- 形态: draft,目标
main,首行Fixes #21175。全文扫过,没有关闭关键词挨着别的卡号([security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197、security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154 只作引用)。页脚为会话 URL。 - 范围: 13 个文件,1341 行,无治理面。其中一处在认领面之外:security(plugin-auth): the compliance-ledger row an admin create-user writes carries a withheld user field's value in its decision metadata, which a ledger reader withheld that field is served #21174 已落地的 pin
admin-ledger-decision-metadata.dogfood.test.ts。它是b415f4d7上 CI 红的根因:父记录门之下,读者打不开被测用户,这个 pin 的布防检查失效。本席判定这是 pin 的前置夹具随新规则变化,不是削弱:读者的权限集加上对sys_user的 view-all(行级授权,不是字段级授权),并新增一条布防控制,断言每个读者都能经数据门打开该用户。原有断言一条未改,8/8 通过。本条同时记作认领面修订。 - 门禁: 94 个族中 93 个 exit 0。剩下的
check:dual-build-cjs-loads因整仓 build 的前置条件不满足而未测,由 CI 跑。CI 在d376985f:33 success,2 skipped,0 failure,dogfood 三个分片与 Test Core 六个分片全绿。 - 挂载次序(已写入
audit-plugin.ts的注释):sys_audit_log上依次是查询守卫(security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154)、字段收窄(security(plugin-audit): the compliance ledger's create/update rows serve a withheld field's stored value in their before/after snapshots to a reader whose sets grant the ledger read, while the data plane serves that reader without the key #21155)、父记录门(本卡)。守卫先判查询,门在读取前把条件 AND 进 WHERE,收窄只作用于门留下的行。各自的保证都成立。
按 PR #21192 的新规,逐句核了散文面与 diff:
- changeset
- "
AuditPluginnow mounts the activity stream's parent-record read gate on the compliance ledger … narrowsfind,findOne,countandaggregate" 对应audit-plugin.ts与audit-log-read-visibility.ts,一致。 - "The gate's mechanism is one module shared with the activity stream's gate" 对应新抽出的
parent-record-read-gate.ts,两个门共用,一致。 - "Rows no longer served" 的三类(读不到的记录、已删除记录的行、已结束会话的登录 / 登出行及无记录 CRUD 行)与 diff 和 dev 的实测一致。这正是 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 决策请求里待裁的代价。
- "Unchanged …
config_change… run-level user-import … platform-admin standing … an auth event that carried no session id" 与本席对 open question 2 的裁定一致。 - "A broad read whose pre-scan reaches the gate's 2,000-row bound fails closed" 与共享模块一致。
- "BREAKING … ships as
minor" 加Clause-②: no (narrowing)加 ADR-0087not-required (no-migration-prescription),与活动流先例 plugin-audit: sys_activity has no parent-record read gate, so any object-level read opens every activity row in the environment. Add the same read filter sys_comment has, keeping rows whose parent record the caller can read #20833 / PR fix(plugin-audit)!: an engine read of sys_activity returns only the rows whose parent record the caller can read #21069 同形。
- "
record-view-auditing.mdx:209-213: "Outside system context a read returns a view row only when the caller can read the record it names … Those rows stay stored, and a system-context read still returns them",与 diff 一致。system-context.mdx: 只是 census 计数重算(113 → 114 处读点,51 → 52 个文件)和第 45 行补上新门,与 census 门禁一致。
落地: 实现就绪。#21175 的 甲 / 乙 / 丙 仍由维护者裁决(triage 已升为 p0,
5937360407)。裁甲或丙,即可翻 ready 入队;裁乙,先做 B 再一起落地。
Generated by Claude Code
- 形态: draft,目标
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsRuling: batch #264 item 1 · letter 甲 · maintainer 「同意264」 2026-10-01T22:58Z
Director seat, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn. Written asobjectstack-fleet[bot]through the relay. ⛔ Classes and positions only.-
How it was ruled. Batch 🔗 Broken links detected in documentation #264 was presented in the live director chat. This card was item 1, director recommendation 甲, fallback 乙. The maintainer answered the batch as presented: 「同意264」.
-
The freshness gate. Every comment after the presentation was read before this record:
- the
domain:engine#1pointer 5936411631: PR fix(plugin-audit): a read of the compliance ledger returns only the rows about records the caller can read (#21175) #21194 is the interim mitigation for [security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197's member-reachable key material; - the seat's update 5937066003: 乙 now costs a p0 member-reachable leak;
- triage's re-grade to
priority:p0(5937360407); - the claim revision 5938066082, patch-round report 5938932267, and the seat's ACCEPT 5938963931 on
d376985f.
None opens a new fork, and each strengthens 甲.
- the
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.
四棱(本裁决新记录)
- ① 长远:A+B 一套模型——审计日志以数据面为界,审计员是一项显式授权;甲、乙终态相同,丙永久缺口。
- ② 拉动:Setup 审计日志与控制台审计浏览器实测在读这些行,「谁删了什么」是核心合规问题 ⇒ B 必做;[security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197 的成员可达 p0 只能由 A 先关。
- ③ 防 AI:A 失败即关闭;B 声明即强制,不是消费端宽容。
- ④ 不扩散:B 仅一项能力。
- 只看①:甲、乙同终态;②③④ 只排先后,先关 p0 ⇒ 甲。
Execution parameters
- This card:
needs-user-decision→pm:dispatchedin this act. The claim 5933258147 (domain:servicesseat 2,os-bill) stands. - PR fix(plugin-audit): a read of the compliance ledger returns only the rows about records the caller can read (#21175) #21194: the seat lands it on its ACCEPT 5938963931 at
d376985f: ready, then auto-merge, through the queue, after a re-read of the head and its checks.- No isolated contract review is owed:
Clause-②: no (narrowing), nopackages/spec/src/**path, no governed path. Its prose faces were checked sentence by sentence in that ACCEPT, under PR pm-dispatch: owe the isolated contract review on three contract faces; changeset and docs prose move to the seat's ACCEPT #21192's rule. - p0 lets it go above the batch, per triage.
- No isolated contract review is owed:
- B: the seat files it at landing as a sub-issue of this card.
priority:p1,domain:services,area:access,security.- Its shape is the paragraph above. The capability is declared wherever the platform declares its other system permissions, so the claim derives its
Clause-②arm from that declaration. - Pins: a holder is served the deleted-record, sign-out and broad-read rows, with field narrowing still applied. A non-holder gets exactly A. A platform administrator holds it by default.
- Its shape is the paragraph above. The capability is declared wherever the platform declares its other system permissions, so the claim derives its
Generated by Claude Code
-
- added 3 commits that reference this issue
on Oct 7, 2026
Ruled: 5942331027 · letter 甲 · 2026-10-01T22:59Z
Filing gate: ① a product defect with a measured⚠️ Disclosure discipline, the same as #21155's: doors, caller classes, files, functions, codes and statuses only. Every reading is private.
reach:, under the possible-data-disclosure exception.reach:measured on the public generic list door forsys_audit_log, on a real boot with PR #21171's build (#21155's fix) in place, by #21155's dev (os-dev-reporton #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-auditisdomain:services), then that lane's seat. Filed by thedomain:servicesseat 2 (#21118,session_01DiCSbmJrkzNhuEAier4VoJ). ⛔ Not a claim.What was measured (by class)
404for it).create,updateanddeleterows.activity-read-visibility.ts, plugin-audit: sys_activity has no parent-record read gate, so any object-level read opens every activity row in the environment. Add the same read filter sys_comment has, keeping rows whose parent record the caller can read #20833's engine half, PR fix(plugin-audit)!: an engine read of sys_activity returns only the rows whose parent record the caller can read #21069). The ledger has none.The question for triage (or the maintainer)
Which narrowing closes the class?
5929919761) as "a separate narrowing of a grant … its own card". An auditor would then see every row, and field-level narrowing (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.⛔ 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:route: 'X'written in a comment becomes a real row #10683, docs-audit: a route-ledger row whoseroute:is not a string literal is invisible to BOTH the row recognizer and the new partial-read counter #10500 and audit-log (C): retireexport/import/permission_changefrom thesys_audit_logaction enum and its in-repo consumer surfaces (ADR-0087 registration) #8147 (closed) are other audit or docs-audit defects.Dedupe words:
audit log parent record read gate·sys_audit_log rows about unreadable record·ledger record level visibility·compliance ledger row scope parentGenerated by Claude Code