Skip to content

feat(service-settings,plugin-audit,plugin-sharing): config_change rows name their settings row; record pages for audit rows and queues - #30728

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-v92xw9
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-v92xw9

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Refs #30209

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:设置写入的 config_change 审计行现在把被改的设置行 id 写进 record_id;sys_audit_log 新增默认记录页,列出「同一记录的其他变更」;sys_queue 新增默认记录页,成员列表按队列 name 过滤,「新建」预填队列名。
  • 为什么改:Setup: show what people can really do and what really changed, using names rather than ids #30209 第 4 行 B 与第 6 行 B 的裁定(6670358877)。审计行有了 record_id,记录页就能把同一设置行的镜像 create/update 行(带非机密明文值)摆在一起;队列成员按名字存,记录页才能直接管成员。
  • 风险与回滚:⚠ 读权限收窄:新的 config_change 行有了 record_id,就归父记录读门管,只持有审计台账读授权、读不到设置表的人从此看不到新的 config_change 行(管理员及持 view_all_audit_log 者照看;旧行 record_id 为空,照旧可见)。席位已接受此后果,维护者可在本 PR 否决。其余是新增页面,不改 sys_queue_member.queue,不碰 spec 与 ui。回滚即撤本 PR。
  • 先看:packages/services/service-settings/src/settings-service.ts 的 upsertRow 与 config-change-audit.ts 的 record_id;packages/plugins/plugin-audit/src/sys-audit-log.page.ts;packages/plugins/plugin-sharing/src/sys-queue.page.ts;packages/qa/dogfood/test/audit-log-parent-read-gate.dogfood.test.ts 新增的读门用例。
  • 两分钟验证:pnpm --filter @objectstack/plugin-sharing exec vitest run src/sys-queue-page.test.ts(8 条全绿)与 pnpm --filter @objectstack/service-settings exec vitest run src/config-change-audit.test.ts(18 条全绿)。
  • 需要你做的:确认是否接受上面的读权限收窄;声明队列(包里声明的队列)不在本 PR 范围。

What changes

Row 4 B: a config_change row names the settings row it changed, and an audit row's record page lists the other changes to the same record.

  • @objectstack/service-settings: SettingsService.upsertRow returns the stored row's id (rowId) from the update branch (the probed row) and the insert branch (the inserted row), and null on the in-memory paths. The audit call passes it as recordId, a new optional field on the SettingsAuditSink.record entry, and buildConfigChangeAuditSink writes it to record_id. object_name already named the store (sys_setting, or sys_platform_setting for a global value). metadata and the digest in new_value are unchanged.
  • @objectstack/plugin-audit: a default full record page for sys_audit_log (sys_audit_log_detail, served through the plugin manifest's pages). It draws every field but id in the object's own field groups, and a record:related_list over sys_audit_log with relationshipField and relationshipValueField both record_id, newest first, with actions: [] so there is no "New" on the append-only ledger. The page label is translated in all eleven locales (translations/pages.ts, wired through withSourceFallback with recorded source hashes and declared in the extract config).
  • The plugin-sharing / plugin-audit: record titles, highlight strips and the by_object grouping of share and audit records name the target object by its API name (object_name), outside the list columns #24501 covers #24611 comment in sys-audit-log.object.ts and the row-class prose in audit-log-read-visibility.ts now say config_change rows name their settings row, and that older config_change rows (and import rows) do not. The gate code is unchanged.

Row 6 B: a queue's record page lists its members.

  • @objectstack/plugin-sharing: a default full record page for sys_queue (sys_queue_detail). Its members list reads sys_queue_member with relationshipField: 'queue' and relationshipValueField: 'name', so the console filters on queue equal to this queue's name and its "New" opens the member form with queue filled in with that name. sys_queue_member.queue is unchanged. The page label ships in the plugin's four locales.
  • Declared queues are out: a package's queue is a registry item with no sys_queue row, so it has no record page. Its organization's member rows stay on the Queue Members list.

Tests

  • service-settings/src/config-change-audit.test.ts (18 pass): the global case pins record_id to the sys_platform_setting row's id, the tenant case to the sys_setting row's id (the re-pin near the old L386), and a new case writes, overwrites and clears one key: one stored row, three ledger rows with the same record_id, and metadata keys exactly as before.
  • plugin-audit/src/sys-audit-log-page.test.ts (10 pass): the manifest carries the page; PageSchema and each node's ComponentPropsMap row parse it (with a control the door refuses); the details cover every field but id; the list props; the eleven locales.
  • plugin-audit/src/sys-audit-log-same-record.integration.test.ts (6 pass), on ObjectQL, the SQLite driver and the real CRUD mirror, with the filter read off the page: beside a global config_change row the list holds both config_change rows and the mirror's create and update; the mirror's update carries the old and new value in plain text (the measure-first reading: yes); a tenant row lists only its own rows; a secret's mirror row carries neither the value nor its handle.
  • plugin-sharing/src/sys-queue-page.test.ts (8 pass), on ObjectQL and SqlDriver over in-memory SQLite: the filter { queue: queue.name } read off the page lists that queue's members and no other's (control: keyed on the id it finds nothing); the "New" prefill { queue: 'billing' } saves a member the list shows; and the list agrees with OwnershipQueueService.resolveQueue on the members.
  • Dogfood (both run, 14 of 14 pass): settings-config-change-audit.dogfood.test.ts adds an end-to-end pin that PUT /api/settings/branding writes a config_change row whose record_id is the stored sys_setting row's id, with the mirror's row about the same write beside it carrying the value. audit-log-parent-read-gate.dogfood.test.ts moves its run-level row to an object-less action invocation (the settings write no longer supplies one, and this boot has no other run-level producer), and adds a pin that the config_change row follows the gate on its settings row: the ledger-only reader gets 404, the admin 200.
  • Ablations through scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty: record_id: entry.recordId ?? null to record_id: null turns 3 config-change-audit cases red; the queue page's relationshipValueField: 'name' to 'id' turns 4 sys-queue-page cases red.
  • Full suites: @objectstack/service-settings 69 files, 1179 tests; @objectstack/plugin-audit 75 files, 1161 tests; @objectstack/plugin-sharing 67 files, 1417 tests; all pass, and tsc --noEmit exits 0 in each.

Acceptance notes

  • Read narrowing, accepted by the seat. A reader who holds only the ledger's read grant no longer sees new config_change rows unless it can read the settings row they name. Holders of view_all_audit_log see every row. The changeset and the 风险 line say this so the maintainer can veto it here.
  • Where the page is reached. Setup's Audit Logs entry opens the audit:log browser drawer, not this record page. The page opens from the object's list and from record links. Linking the drawer to the page is a ui change, outside this PR.
  • No ui fallback was needed. The console's related list has no limit on a self-referencing list. A row with no record_id sends no query and shows the empty state. The list includes the row being read.
  • Left unchanged: the config_changes list view's columns; the fixture in audit-log-deployment-level-rows.integration.test.ts, whose null-record_id config_change row tests tenant_id, not the record pointer; and open PR fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448, which edits setMany's env-lock branch in the same file but not upsertRow or the audit call.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 69 commands on head f6c2de4 (origin/main merged in). Each ran with its exit code captured before any pipe: 69 exit 0. check-plugin-teardown-shape --self-test exited 3 on the first pass (shallow clone, the pinned fixture commit unreachable); after fetching 621a4876 it exits 0. --ran reconciliation: "69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)".
  • pnpm lint: exit 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cyf254wVgypzQ5Uj1a

…gs row it changed

upsertRow reports the id of the stored row a write landed on, and the
config_change sink writes it as record_id beside the object the scope
selects, so the ledger row and the CRUD mirror's row about the same write
share one pointer pair.

Claude-Session: https://claude.ai/code/session_016WV4cyf254wVgypzQ5Uj1a
Co-authored-by: Claude <[email protected]>
…bout the same record

The sys_audit_log record page draws the row's fields in their groups and,
beneath them, every ledger row whose record_id is this row's, so a
config_change row's digest sits beside the CRUD mirror's row that carries
the value. The page label is translated in the eleven shipped locales, and
the read-gate prose names config_change among the rows about a record.

Claude-Session: https://claude.ai/code/session_016WV4cyf254wVgypzQ5Uj1a
Co-authored-by: Claude <[email protected]>
…e name

The organization's queue gets a default full record page whose members list
reads sys_queue_member keyed on `queue` = this queue's `name`
(relationshipValueField), so the console's filter and its "New" prefill both
carry the name the member rows store. sys_queue_member.queue is unchanged.
Declared queues have no sys_queue row and so no page.

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_016WV4cyf254wVgypzQ5Uj1a
…n-level ledger row to an object-less action

A settings write's config_change row now names the settings row it changed,
so the parent-read-gate fixture loses its only row about no record. It now
gets one from the action door (an action invoked with no subject record),
and pins that the config_change row follows the gate on the settings row
it names while the admin is still served it. The settings audit fixture
pins record_id against the stored sys_setting row and finds the mirror's
row about the same write beside it.

Adds the changeset for the three packages.

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_016WV4cyf254wVgypzQ5Uj1a
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

席位核对 · ACCEPT

PR #30728 · head f6c2de44 · 2026-11-05T23:43Z · card #30209 rows 4B + 6B (claim 6671520435, amendment 6671566161) · os-dev report 6672470118

维护者速读(席位校正)

The body's lines stand as written, read against the diff. One consequence is the ruling's own and is named in the 风险 line and the changeset: a reader holding only the ledger's read grant, who cannot read the settings store, no longer sees NEW config_change rows. view_all_audit_log holders (admins by default) see every row; older null-record_id rows are unchanged. Nothing becomes visible that was not before. Veto at this PR if that narrowing is not wanted.

席位核对

  • Shape: read the diff myself (22 files, +1123 / −78). upsertRow returns rowId (probed row on update, insert result on insert, null for the in-memory store); the sink records it as record_id beside the unchanged object_name; metadata and the new_value digest are unchanged. sys_audit_log_detail lists ledger rows keyed record_id ↔ record_id, newest first, no "New" (ledger is get/list only). sys_queue_detail lists sys_queue_member keyed queue ↔ name via the published relationshipValueField (component.zod.ts), so the member column and ADR-0142's name key are untouched. Both pages are kind: 'full', isDefault, labels in each plugin's shipped locales.
  • Read gate: audit-log-read-visibility.ts only moves config_change from the "about no record" class to the record-gate class in its header; the gate's code path already judges any row with a record_id, so the new rows ride the existing gate and the field redaction, as the CRUD mirror's rows about the same write already do.
  • Narrowing question (report 6672470118): Clause-②: no, no (narrowing) arm. The narrowing is runtime read visibility of ledger rows under an existing gate, not a change to a published contract's acceptance set: no schema, key, status or accepted input changes. It is disclosed in the changeset and 速读 above instead.
  • Declarations: Refs #30209 (rows 1–3, 5 stay open on the card), Clause-②: no, Metadata-protocol: none, patch changeset on the three packages; consistent with the diff.
  • Out of scope / follow-up: the console's Audit Logs drawer does not link to the new record page; the seat files a ui card at landing.
  • Landing: check-governed-merges --pr 30728: NOT governed, 1201 changed lines (≤ 3000). CI on this head is still running; the seat lands with pr_ready + automerge_enable once it is green or expected-skip.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-audit, @objectstack/plugin-sharing, @objectstack/service-settings, touching 39 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-audit/src/audit-log-read-visibility.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

58 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ab44ec94b0362bafdf5f610b1d121655cd39a5fa.

⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-audit/src/audit-log-read-visibility.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 43 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 22 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 ab44ec94b0362bafdf5f610b1d121655cd39a5fa → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1f4c72d067f7886d24ae81d1b912f94eb87d4ba — the merge of head f6c2de448e069709b45b53d3b75055dd2df805a3 into base ab44ec94b0362bafdf5f610b1d121655cd39a5fa, 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 a1f4c72d067f7886d24ae81d1b912f94eb87d4ba && git checkout a1f4c72d067f7886d24ae81d1b912f94eb87d4ba
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab44ec94b0362bafdf5f610b1d121655cd39a5fa f6c2de448e069709b45b53d3b75055dd2df805a3 && git checkout -B drift-repro ab44ec94b0362bafdf5f610b1d121655cd39a5fa && git merge --no-ff f6c2de448e069709b45b53d3b75055dd2df805a3

node scripts/docs-audit/affected-docs.mjs --json ab44ec94b0362bafdf5f610b1d121655cd39a5fa

⚠️ 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 ab44ec94b0362bafdf5f610b1d121655cd39a5fa → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant