Repository navigation
feat(plugin-audit, rest): a query of an opted-in saved dataset writes one dataset_query audit row - #33559
feat(plugin-audit, rest): a query of an opted-in saved dataset writes one dataset_query audit row#33559objectstack-fleet[bot] wants to merge 11 commits into
Conversation
…et query, none when refused before the read Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
… opted-in saved dataset The analytics dataset door hands the audit slot one event per saved-dataset query that reached the read, answered or failed; a request refused before the read hands none. plugin-audit records only the datasets named in AuditPlugin's datasetQueryAudit.datasets (off by default), batched off the request path with the record-view writer's batcher, and keeps the filter's field names and operators, never its values. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
…changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
…lection Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
…ssion import Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
…xt readably; the tenant audit census counts the new site Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
📓 Docs Drift CheckThis PR changes 2 package(s): 57 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 18 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 27 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 e5d8edd608d9bdfe5221b0eafc4aeee334e5e6df && git checkout e5d8edd608d9bdfe5221b0eafc4aeee334e5e6df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4bba8b8f02fdbdf30e9ad41fc5d2a6fdd3ccaf2a be4a48ae3044fd52058048c8508b3d5fc877c57c && git checkout -B drift-repro 4bba8b8f02fdbdf30e9ad41fc5d2a6fdd3ccaf2a && git merge --no-ff be4a48ae3044fd52058048c8508b3d5fc877c57c
node scripts/docs-audit/affected-docs.mjs --json 4bba8b8f02fdbdf30e9ad41fc5d2a6fdd3ccaf2a
|
|
Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 1f74187 · 2026-11-15T01:44Z review#3. |
Contract reviewServed-tier: The PR's own commits ( ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL |
… the dataset_query row Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
…ng the dataset it read, and a served draft Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
…ert order within one batch Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM
|
Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head be4a48a · 2026-11-15T02:15Z review#3 (delta). |
Contract reviewServed-tier: This reviews the delta on my FAIL at ① Derived judgments
② Semver levelUnchanged and correct: ③ Boundary flagsThe ACCEPT re-reads every check at the landing head. Implemented-by: VERDICT: PASS |
Requested by Elon · project thread
domain:server#5 · os-elon-musk · session_016WV4cz2MxDkQYCC4U8indM
Fixes #31797
Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: none — no file under
skills/**teaches the audit ledger's actions orAuditPlugin's options;content/docs/permissions/record-view-auditing.mdxgains a "Dataset queries" section.Maintainer quick read
new AuditPlugin({ datasetQueryAudit: { datasets: [...] } }). Each query of one of them through the analytics dataset door then leaves onedataset_queryrow insys_audit_log: who ran it, which dataset, the dimensions and measures selected, the filtered field names and operators (never the values), the row count, and whether it succeeded.packages/rest/src/analytics-dataset-query-audit.tsrunAuditedDatasetQuery: only the read is wrapped, so a refusal before it writes nothing; (2)packages/plugins/plugin-audit/src/dataset-query-audit.tsbuildDatasetQueryAuditRow: what the row holds and that the filter goes throughsummarizeExportFilter; (3) thequeryDatasethunk inpackages/rest/src/analytics-routes.ts.pnpm --filter @objectstack/plugin-audit exec vitest run src/dataset-query-audit.test.ts. Expect 10 passing cases, among them "a dataset that is not opted in writes no row" and "records no filtered value".What changes
plugin-audit. A new writer,
dataset-query-audit.ts, emitsaction: 'dataset_query'. Each row is a run-level row:record_idnull, the dataset's base object inobject_name,user_id/actor/tenant_idfrom the caller's envelope.metadataholdsdataset,dimensions,measures,time_dimensions(names only, when selected),filter(field and operator terms from the export writer'ssummarizeExportFilter, every comparand dropped unread),row_countandoutcome. A failed query addsstatusanderror_code. The delegated-call pair andimpersonated_byare written as the export row writes them. The writer follows the scope pinsread-audit.tsset for #8992:AuditPlugin's newdatasetQueryAuditoption (datasets,maxBatchSize,flushIntervalMs) has the same shape asreadAudit. No spec change: it is plugin configuration, not authorable metadata, soMetadata-protocol: none. A dataset that is not named is dropped before anything is buffered. An empty list builds no batcher.recordDatasetQueryon theauditslot builds the row when it is called (names and counts only, so the buffer never holds the caller's filter), enqueues it and returns. Rows are written bycreateReadAuditBatcher, the record-view writer's batcher, which now takes an event type parameter and an optional overflow sentence. The query instant is kept throughpreserveAudit, as on record views. A lost batch is reported aterroronce per cause. The named calleepersistDatasetQueryAuditRowsis registered incheck:durability-log-leveland in the swallow census.sys_audit_log.actionoptions gainDataset Query/dataset_query, with the generated translation leaves for the 11 bundles. The enum ledger (sys-audit-log-retired-actions.test.ts) and the label pin list it with its writer. Its shipped surfaces are the unfilteredrecentandall_eventsviews, as forscim_refused.rest.
answerDatasetQuerywraps only thequeryDatasetcall inrunAuditedDatasetQuery(newanalytics-dataset-query-audit.ts). The event shape is declared structurally, as the export door declares its own. The rules are #31000's, asdata-export-audit.tsimplements them:failed, plus the status and machine coderesolveThrownHttpErrorreads, never the error's text;AnalyticsRouteHostgainsresolveAuditService, andrest-server.tshands it over.Overlap with open PRs. #33491 edits
analytics-routes.ts(the import ofadmitRefusedDatasetHolders, the refusal-arm hunks near the resolution). This PR's three hunks in that file are elsewhere: one import afterstripReadDecorations, one member at the end ofAnalyticsRouteHost, and thequeryDatasetcall.git merge-treeof this head with each of #33491 and #33512 reports no conflict.Tests
The pins were committed first (
4cdb3b2df) and run red before the fix:plugin-auditsrc/dataset-query-audit.test.ts:Error: Cannot find module './dataset-query-audit.js'. No writer existed.restsrc/analytics-dataset-query-audit.test.ts:Tests 3 failed | 3 passed (6). Red: one event per answered query, one failed event after the read started, one event per batch query that reached the read. The three green cases (no event for a refused request, none for an inline definition, a throwing sink does not change the answer) are pins on the absence of an event, so they were green before the door handed any event.After the fix, with module-level evidence in Verification:
AuditPluginand theauditslot);queryDatasetnever called);failedrow and event after the read started (503 withSERVICE_UNAVAILABLE), the error's text not carried;$in,$contains, a nested relation path and an implicit equality);created_at; the enqueue returning before the write; a lost batch reported once per cause.Acceptance notes
nameis text the caller chose. Recording inline queries would need a different opt-in key (the base object, say), which this ruling does not decide. The docs section states this edge.queryDatasetis recorded as a failed read. The analytics service judges object exposure and dataset fields insidequeryDataset, and the door cannot tell that throw from a driver fault without reading the service's internals. It follows the export door's precedent, where a refusal fromfindDataafter the export began is afailedrow with its status and code: the row saysoutcome: failedwith the status and machine code the door answered (a 403 for an exposure refusal). No case pins a 403 row; the 503 case pins the mechanism.auditslot for every saved-dataset query. The opt-in list lives in the writer (one input, the record-view writer's rule), so the door cannot skip the lookup. The cost is a service lookup; the write itself is batched.migrations/entries/semantic/17.audit-log-action-enum-retired.tsretiresexport/permission_change/restoreonly,packages/spechas no mirror of the action options, and no other ledger lists them (git grep -n scim_refusedreaches only plugin-audit and plugin-auth).Verification
Package suites and typecheck
Run on the branch before the last two commits (
3d982b0ac, then0f3569f23for the rest re-run). The last commit1f741879aonly drops twoas anycasts on the writer's insert and refreshes the tenant audit census; after it,src/dataset-query-audit.test.ts+src/read-audit.test.tswere run again (Tests 43 passed (43)) and plugin-audittsc --noEmitexited 0.pnpm --filter @objectstack/plugin-audit test:Test Files 94 passed (94),Tests 1505 passed (1505).pnpm --filter @objectstack/plugin-audit typecheck: exit 0 (lock VERDICT command-exit 0).pnpm --filter @objectstack/rest test:Test Files 412 passed (412),Tests 8399 passed | 332 skipped (8731).pnpm --filter @objectstack/rest typecheck: exit 0 (lock VERDICT command-exit 0). After moving the import,analytics-dataset-query-audit.test.ts+analytics-routes.test.tsran again (Tests 83 passed (83)) and resttsc --noEmitexited 0.--no-inline-configon every changed.ts/.mjsfile: exit 0.node scripts/check-test-source-alias.mjs: exit 0 after the new rest pin gained its module-top@objectstack/objectqlimport, which the gate asked for.node scripts/check-nul-bytes.mjs: exit 0.pnpm --filter @objectstack/metadata-protocol exec vitest run src/sys-metadata-repository.draft-drain.test.ts, which reads the edited durability gate script:Tests 11 passed (11).Derived gate families
node scripts/pm/dispatch-gates.mjs --commandsderived 127 families for this diff, and all 127 were run (--ran: 127 derived, 127 run, 0 unrun). Each exit code was captured before any pipe. 121 exited 0 on the first pass. The other six:check-tenant-audit-censusand its--self-test: exit 1. This diff adds one write call site (dataset-query-audit.ts, insert intosys_audit_log, elevated), so the census moved from 314 to 315. Fixed bynode scripts/tenant-audit-census.mjs --writeplus the six hand-written figures oncontent/docs/permissions/tenant-audit-census.mdx; both re-run exit 0 (315 write call sites certified, self-test27 cases pass).pnpm check:i18n: exit 3, a prerequisite (CLI not built). I built the closure the gate names and re-ran it: exit 0,all bundles in sync.check-plugin-teardown-shape --self-test(exit 3, shallow clone cannot reach its pinned positive control),check:dual-build-cjs-loads(exit 3, needs a whole-repo build), andspec check:skill-examples(exit 3, needs the client SDKdist/). None of them reads a file this diff changes in a way it depends on; CI runs all three on a full checkout.The families ran against the tree before
1f741879a. That commit changes the writer's insert options and the census page, and only the census gate was re-run on it.Human Minimum
packages/specholds no runtime logic. (Nopackages/specchange.)pnpm lintpasses. (eslint on the changed files exit 0; the repo-wide run is CI's.)main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM