Repository navigation
fix(plugin-audit): a user's own UI-state write writes no ledger row - #23866
Conversation
writeAudit now returns before building either row when the write is a user's own write to their own per-user UI-state row (isOwnUiStateWrite on the closed PER_USER_UI_STATE_OWNER_FIELD list, today sys_user_preference). Another actor's write (an administrator, a system write, a write that moves the row to another owner) still writes the ledger row and the activity row. Ruling record 6212163130, letter B. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
The first ablation of the ledger skip also reddened the administrator, system and move cases, only because their seed is the owner's own create. Each case now measures the rows its write under test adds. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
…n-ui-state-no-ledger-row
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 66496d75deb7bce6f254cf9320eb9a96461be1df && git checkout 66496d75deb7bce6f254cf9320eb9a96461be1df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f7e836da1ed344aaa6936b9de8b173d0f326b9ec c98019b0eceda8918fd52df7a37725132ac9cb39 && git checkout -B drift-repro f7e836da1ed344aaa6936b9de8b173d0f326b9ec && git merge --no-ff c98019b0eceda8918fd52df7a37725132ac9cb39
node scripts/docs-audit/affected-docs.mjs --json f7e836da1ed344aaa6936b9de8b173d0f326b9ec |
The kernel, ObjectQL, SQLite and AuditPlugin imports took the isolated load of audit-writers.test.ts from about 12.5 s to about 17 s. The block moves verbatim (docblock opening reworded) into audit-writers.own-ui-state.integration.test.ts; audit-writers.test.ts keeps the fake-engine pins and returns to the base file's imports. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · ACCEPT ·
|
Fixes #23804
Clause-②: no
What this changes
writeAudit(packages/plugins/plugin-audit/src/audit-writers.ts) now returns before it builds either row when the write is a user's own write to their own per-user UI-state row. The test is the existingisOwnUiStateWriteover the closed in-plugin listPER_USER_UI_STATE_OWNER_FIELD, todaysys_user_preferencemapped touser_id. Until now that test gated only the activity mirror (the #22961 change, PR #22996), so thesys_audit_logrow was always written. This executes ruling record 6212163130 (letter B, the maintainer's 「同意」), on face 6209004397 and director delta 6211505939.userId: the ledger row'suser_id, the activity row'sactor_id) must be the owner on every side of the row that names one. No side naming an owner answersfalse. Another actor's write still writes the ledger row and the activity row: an administrator from Setup, a system write with no user, and a write that moves the row to another owner. Nothing about what the test admits was widened.userIdis resolved and before any read the writer pays for. The activity gate went back toenable.activitiesalone, because the own-write case never reaches it now.AUDIT_EXCLUDED_OBJECTS/excludeObjects/SKIP_OBJECTS(the skip is a condition insidewriteAudit, not an object exclusion);packages/spec(object.zod.ts:259is untouched);packages/platform-objects; objectui. Stored rows stay, and nothing is migrated.PER_USER_UI_STATE_OWNER_FIELDis now the ledger-and-activity rule and cites ruling record 6212163130. The file-level skip-rule bullet says the same..changeset/23804-own-ui-state-no-ledger-row.md,patchfor@objectstack/plugin-audit. Its first paragraph says the ledger no longer records a user's own UI-state writes, and names the consumer this can surprise: an export ofsys_audit_logto a SIEM or compliance archive.sys_audit_log_metricsalready readsconfig_change).Measured before writing the pins
On a real kernel (
ObjectKernel+ObjectQLPlugin+AuditPlugin), a real SQLite driver and the shippedsys_user_preference, a one-off probe recorded what the engine hands the hook. The probe was not committed.session.userIdresultpreviousisOwnUiStateWriteuser_id= owneruser_id= owneruser_id= ownertrue(not evidence)user_id= owneruser_idto null is refused by the engine (VALIDATION_FAILED, "User is required and cannot be cleared"), so no write can un-own a row past the test.sys_audit_loginserts areauth-event-audit.ts(session events),plugin-auth/src/admin-import-users.ts(importrows aboutsys_user),service-settings/src/config-change-audit.ts(config_changerows onsys_setting/sys_platform_setting) andread-audit.ts(readrows for opted-in objects). None writes a row for asys_user_preferencewrite, and none was changed.Clause-②: no).dist/index.d.tsanddist/index.d.mtsbuilt from this head and from the base'saudit-writers.tsare byte-identical (cmp), and the restore was proven by blob hash equal to HEAD.Pins
In two test files,
audit-writers.test.ts(fake engine) andaudit-writers.own-ui-state.integration.test.ts(real engine):sys_audit_logrow and nosys_activityrow. The three CONTROL writers (an admin, a system write, a move to another owner) are unchanged and still assert one ledger row and one activity row.user_idnull;sys_sharing_rule→ one ledger row (the card's Acceptance 1).Ablations (committed head
5ff57252e2; the suite readssrc/through relative imports, so nodist/leg)Both used
node scripts/ablation-replace.mjsin wrap mode. The tool verified the anchor count, the blob change and the restore (blob equal to HEAD,git diff HEADempty) on disk.Delete the ledger skip (
if (isOwnUiStateWrite(ctx.object, userId, before, after)) return;, anchor 1 → 0): 6 failed / 101 passed. The failures are exactly the six own-write pins, three fake-engine and three real-engine, for exampleexpected [ { id: …, …(15) } ] to have a length of +0 but got 1.5ff57252e2, and the run above is on that head.Remove the owner equality (
return owners.length > 0 && owners.every(…)→return owners.length > 0;): 6 failed / 101 passed:expected [] to deeply equal [ [ 'update', 'u_admin' ] ]ataudit-writers.test.ts:899and:914.The system-write pins and the
sys_sharing_rulepin stayed green, as predicted: the acting-user check survives that mutation.Reverse reading at no extra cost: the base's
audit-writers.test.ts, run against this head's source, fails exactly its three own-write pins (expected [] to have a length of 1 but got +0).Tests and gates (head
03ba0dc800, aftermainata31ff2eb5dwas merged in)Every heavy run went through
scripts/pm/os-verify-lock.sh. The verdict lines are quoted.pnpm --filter @objectstack/plugin-audit test: 51 files, 840 tests passed (VERDICT command-exit 0).pnpm --filter @objectstack/plugin-audit typecheck:VERDICT command-exit 0. The test layer compiles with 0 errors.tsc -p tsconfig.test.json --listFileslistssrc/audit-writers.test.ts(count 1), so the typecheck reaches the new block.vitest run src/audit-writers.test.ts: 107 passed (at5ff57252e2, the head the ablations used).turbo run build --filter='@objectstack/plugin-audit^...':VERDICT command-exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackgave 68 commands for these 3 paths. All 68 ran with the exit code captured before any pipe.--ranreconciliation: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:i18n(no built CLI) andpnpm check:dual-build-cjs-loads(no workspacedist/). After their prerequisite builds (turbo replays, 61 of 61 and 74 of 75 cached), each was re-run and exited 0. The--ranlist carries the re-run codes.pnpm test:repowas not run locally (CI'sTest Coreruns it): this diff changes no file in that project.Acceptance notes
A delegated write counts as the owner's. For an MCP client acting for the owner (ADR-0090 D10),
session.userIdis the delegator, so that write to the owner's own row is skipped too. Themetadata.performed_bythe ledger would have carried is not written. This is the same predicate the activity half has used since plugin-audit: a user's own sys_user_preference writes (ui.recent) come back to them as "Updated User Preference 'ui.recent'" inbox notifications — the console bell shows six of them and nothing else #22961. I did not measure whether any agent writessys_user_preferencetoday.An attributed system write counts as the owner's. A system-authorized write whose
provenance.attributedUserIdis the owner is judged as the owner's own write (userIdfalls back to it), as on the activity half. The producers ofattributedUserIdare plugin-auth's better-auth adapter and admin flows and plugin-security's auto org-admin grant, and none of them writessys_user_preference.A user's own delete is rarely reachable. The default permission sets grant
sys_user_preferencewithallowDelete: false(plugin-security/src/objects/default-permission-sets.ts:821), so the own-delete case is mostly a principal with delete rights deleting their own row.This test file got heavier to load. The new block adds static imports (kernel, ObjectQL, the SQLite driver, the identity objects,
AuditPlugin) toaudit-writers.test.ts. Isolated single-file runs on this shared box:The base file was already over
FILE_BUDGET_SECONDS(10 s) in this isolated reading. In the package run, transforms are shared across files (51 files, 65.66 s). The reading is report-only. Carrier: the shard-timings refresh lane, from its CI window.Labels: zero writes. The dispatch names no label, and
skip-changesetdoes not apply (this PR carries a changeset).Card relations. This PR closes only the card on its first line. plugin-audit: a user's own sys_user_preference writes (ui.recent) come back to them as "Updated User Preference 'ui.recent'" inbox notifications — the console bell shows six of them and nothing else #22961 and PR fix(plugin-audit): a user's own sys_user_preference write no longer reaches their bell as a sys_activity row #22996 are cited as the precedent seam and are not addressed here.
Generated by Claude Code