Repository navigation
Description
repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2
Filing gate: (b) a contract gap with a named consumer. The cloud#3665 dev measured it (report 6686540441 on objectstack-ai/cloud#3665, Enterprise P2-4). The maintainer ruled on that card 「企业版授权,用平台导出(推荐)」: the Enterprise audit-export is a licensed permission set over this framework's export door, so the door itself has to record the export. Platform gaps go to the platform (「平台的问题应该在平台处理」, objectstack#30765). Consumer: objectstack-ai/cloud#3665's export PR, which waits on this card.
维护者速读
现在通过平台的「导出」(GET /data/:object/export,csv/xlsx/json)导出任何对象的数据,审计日志里都不会留下一条记录;审计日志的动作枚举里也已经没有 export 这个值(#8147 因为"从来没人写"把它退役了)。企业版要求"导出审计日志这件事本身也要被审计",所以需要平台在导出门上写一条审计记录,并把 export 值和写入方一起恢复。
Measured (objectstack 6ddf827236)
packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts:289retiredexportfrom theactionenum under ADR-0087 (audit-log (C): retireexport/import/permission_changefrom thesys_audit_logaction enum and its in-repo consumer surfaces (ADR-0087 registration) #8147, ruling 2026-08-12), because nothing wrote it. The:244view filter dropped it too.packages/rest/src/data-action-routes.ts:1096(GET /data/:object/export, the streaming export) writes nosys_audit_logrow.- On a real objectos-ee boot the door answers
403 EXPORT_NOT_PERMITTEDonsys_audit_loguntilallowExportis granted. So the permission gate works; only the record is missing.
Expected
- One
sys_audit_logrow per completed export, for every object (not just the ledger), in the same PR as the restoredexportenum value. ADR-0087's 宁窄勿谎 rule allows a value only together with its writer. - The row carries:
- actor and user
- object
- format
- row count
- truncation (the 50k cap)
- the filter / query as a bounded summary that never includes cell values
- outcome
- A refused export (
EXPORT_NOT_PERMITTED) records either nothing or a refusal row. Say which, and why. - Follow the incumbent run-level shape (
record_id: null, asplugin-auth'sadmin-import-users.tsimportrow does) unless the lane judges otherwise.
Acceptance
- Tests:
- An export of an ordinary object writes one row with format and count.
- An export of
sys_audit_logwrites one row. - A truncated export records the truncation.
- The row never carries an exported cell value.
- The ADR-0087 / audit-log (C): retire
export/import/permission_changefrom thesys_audit_logaction enum and its in-repo consumer surfaces (ADR-0087 registration) #8147 comments atsys-audit-log.object.ts:244and:289are updated in the same PR.
Suggested-grade: p1 · Enterprise priority (maintainer, 「cloud 多了几个企业版的任务,优先处理」).
Dedupe (2026-11-06): export audit row → 1815 hits, top 5 none (#30924 is sign-in refusals); audit export action → 956, top 5 none; data export audited → 140, top 5 none (all closed, unrelated); export sys_audit_log → 116, top 5 none (#24899 is the archive copy, closed). None covers this gap.
Activity
objectstack-fleet commentedon Nov 6, 2026
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW
Claim: PM loop patrol · 2026-11-06T14:27Z
Session:
session_016WV4cytLmJxfAmgPVjUbbWAccount:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-30955-export-audit-row(placeholder; the card thread's harness-pinned branch replaces it by a full re-post of this claim before any push)Worktree:
objectstack-issue-30955Domain:
domain:serverSeat:
domain:server#1File surface (card Expected and Acceptance, nothing else):
packages/rest/src/data-action-routes.ts(theGET /data/:object/exportdoor writes onesys_audit_logrow per completed export, run-level,record_id: null) and the audit writer it reaches, following the incumbent run-level writer (plugin-auth'sadmin-import-users.tsimportrow); their tests.packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts: restore theexportvalue ofactionand the view filter at about :244, with the ADR-0087 / audit-log (C): retireexport/import/permission_changefrom thesys_audit_logaction enum and its in-repo consumer surfaces (ADR-0087 registration) #8147 comments at :244 and :289 updated in the same diff; the option's label in plugin-audit's translation bundles if the enum's labels are bundled..changeset/files at the levels the diff earns.⛔
packages/spec/**(stop and ask if the restored value needs a spec enum),docs/adr/**,ui/**,skills/**,.claude/**, the export door's permission gate (EXPORT_NOT_PERMITTEDstays as it is), objectstack-ai/cloud. ⛔ No exported cell value in the row, its metadata, a log line or an error: the filter is recorded as a bounded summary of field names and operators only. Stop on breach and explain in the reportContainer & model:
M,mode:card-thread,model: default,effort: xhigh(p1, an audit row beside exported data)Clause-②: yes, expected (widening: a served audit
actionvalue comes back, with its writer). The dev re-derives it from the diffMetadata-protocol: none expected; the dev re-derives it
Responsibility: what the data export door records |
sys_audit_log.action(export) | audit readers (the audit log list, the Enterprise audit export in objectstack-ai/cloud#3665), every caller ofGET /data/:object/exportThread-read: none
Premise: as the filer measured on
6ddf827236:sys-audit-log.object.ts:289retiredexport(#8147, nothing wrote it), anddata-action-routes.ts:1096writes nosys_audit_logrow. The dev re-reads both on currentmain, and says whether a refused export records nothing or a refusal row, and whyOwed before landing: a
CONTRACT_REVIEW_TIERreview (a served enum value restored)Serial constraints cleared: an open-PR search for the export door and the audit
actionfinds only #30950 (plugin-auth session audit), which touches none of these files; the dev rebases on a collisionobjectstack-fleet commentedon Nov 6, 2026
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW
Claim: PM loop patrol · 2026-11-06T14:34Z (full re-post of claim 6687178882: the card thread's branch, and the file surface amended to the writer's reach the dev declared before the first push)
Session:
session_016WV4cytLmJxfAmgPVjUbbW(the seat); dev = card threadsession_016WV4cxfxbKV82MKKQzizpPAccount:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-y3qc8b(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree:
objectstack-issue-30955Domain:
domain:serverSeat:
domain:server#1File surface (card Expected and Acceptance, nothing else):
packages/rest/src/data-action-routes.ts(theGET /data/:object/exportdoor hands the audit service an export EVENT, never a row, after a completed export), and the reach to theauditslot:packages/rest/src/rest-api-plugin.ts(an environment-scoped provider foraudit, the same helper as security and i18n),rest-server.ts(the provider appended last to the constructor, per [decision · p0] an ex-member API key reads AND writes another organization's rows on the single-kernel wiring underisolated— the wall compares against the caller's own unvetted claim #15256, andresolveAuditServicehanded toDataActionRouteHost),rest-api-plugin-slot-lookups.test.ts(the PROVIDERS table); their tests.action_invocation(audit: an action run through MCP leaves no audit row naming its surface; ruling 6513869839 parameter 3 (#27681) needs an execution-context surface key, the audit writer and the forward headers #27908): a newpackages/plugins/plugin-audit/src/data-export-audit.ts(createDataExportAuditWriter, memberrecordDataExport) and its test, merged onto the existingauditslot inaudit-plugin.ts, exported fromindex.ts. The row is the incumbent run-level shape (plugin-authadmin-import-users.tsimportrow):action: 'export',record_id: null.packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts: restore theexportvalue ofactionand the view filter at about :244, with the ADR-0087 / audit-log (C): retireexport/import/permission_changefrom thesys_audit_logaction enum and its in-repo consumer surfaces (ADR-0087 registration) #8147 comments at :244 and :289 updated in the same diff;objects/sys-audit-log-retired-actions.test.ts; the option's label in plugin-audit's translation bundles.scripts/check-durability-degradation-log-level.mjs: oneDURABILITY_CRITICAL_CALLEESentry for the writer's persist callee, with its self-test run..changeset/files at the levels the diff earns.⛔
packages/spec/**(stop and ask if the restored value needs a spec enum),docs/adr/**,ui/**,skills/**,.claude/**, the export door's permission gate (EXPORT_NOT_PERMITTEDstays as it is), objectstack-ai/cloud. ⛔ No exported cell value in the row, its metadata, a log line or an error: the filter is recorded as a bounded summary of field names and operators only. Stop on breach and explain in the reportContainer & model:
M,mode:card-thread,model: default,effort: xhigh(p1, an audit row beside exported data)Clause-②: yes, expected (widening: a served audit
actionvalue comes back, with its writer). The dev re-derives it from the diffMetadata-protocol: none expected; the dev re-derives it
Responsibility: what the data export door records |
sys_audit_log.action(export) | audit readers (the audit log list, the Enterprise audit export in objectstack-ai/cloud#3665), every caller ofGET /data/:object/exportThread-read: 6687178882
Premise: as the filer measured on
6ddf827236:sys-audit-log.object.ts:289retiredexport(#8147, nothing wrote it), anddata-action-routes.ts:1096writes nosys_audit_logrow. The dev re-reads both on currentmain, and says whether a refused export records nothing or a refusal row, and whyOwed before landing: a
CONTRACT_REVIEW_TIERreview (a served enum value restored)Serial constraints cleared: an open-PR search for the export door and the audit
actionfinds only #30950 (plugin-auth session audit), which touches none of these files; the dev rebases on a collisiontest(rest): the export door hands the ledger one event per streamed e…
test(dogfood): an export through the real door, gate and ledger leave…
chore(changeset): #30955 export audit row — plugin-audit and rest patch
objectstack-fleet commentedon Nov 6, 2026
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW
Seat handoff: the lane signs off. PR #31000 is a draft (head
63e2ee7f56) and has had no contract review. On the maintainer's word, server seat 2 takes over the review and the landing (mailbox #30985). The card thread's handoff follows.#30955 handoff: data export audit row
Written 2026-11-06 by the #30955 card thread (session_016WV4cxfxbKV82MKKQzizpP) for the next server seat. The server 席1 lane signed off; no contract review has been done.
State
sys_audit_logrow, and theexportaction comes back with its writer #31000, a draft opened through the relay. Its body was read back byte-identical, and the assignee is os-elon-musk.claude/project-thread-y3qc8b, head63e2ee7f56(9 commits). Main1b3e22f02cis merged in.WRITE_SHAPED_CALLEESline inscripts/measure-durability-swallow-family.mjs.Fixes #30955,Clause-②: yes (widening),Metadata-protocol: none..changeset/30955-data-export-audit-row.md, patch for@objectstack/plugin-auditand@objectstack/rest.What the PR does
GET /data/:object/exporthands one event to theauditslot'srecordDataExportfor each export that passed its gates and began reading. The event is handed after the response has ended.data-export-audit.tswrites one run-levelsys_audit_logrow from it:action: 'export',record_id: null. The metadata carries:searchas a boolean, never the term;EXPORT_NOT_PERMITTED, a 400 parameter or filter refusal, or the template.exportis restored on the enum and on theconfig_changesview.Local verification on the final head
NODE_OPTIONS=--max-old-space-size=6144, as CI sets it.pnpm lintexit 0.exportaction value is retired, so no export of any object (the audit ledger included) can be audited #30955.Ablations and the main re-measure are in the PR body.
What's left
CONTRACT_REVIEW_TIER, because a served enum value is restored. Nobody has done it.sys_audit_logrow, and theexportaction comes back with its writer #31000. Not read by this thread, and no CI fix rounds have been done.outcome: 'failed'row (404 OBJECT_NOT_FOUND), but only when the export gate let the caller through for that name. Making it a no-row refusal is a one-line special case indata-action-routes.ts. This behaviour was probed, not pinned.ui/packages/plugin-detail/src/HistoryTimeline.tsxhas a comment callingexportretired.17.audit-log-action-enum-retired.ts(17.x history) is spec ⛔.audit-log-read-visibility.tsandaudit-log-field-redaction.tsdon't name the writer.Census race with #30946 (#30918)
Both PRs add one tenant-audit write call site, and both move the census from 261 to 262. Whichever lands second will conflict or go red on
check-tenant-audit-census. The second one should:node scripts/tenant-audit-census.mjs --write. It should then read 263, plus whatever else landed.content/docs/permissions/tenant-audit-census.mdx, which the gate holds to the census: write call sites, decidable, decidably elevated, and the2 / Ncitation. Then runnode scripts/check-tenant-audit-census.mjsuntil it is OK.Traps met on this card
@objectstack/restand@objectstack/plugin-auditthrough their BUILT dist. After any source swap (re-measure, ablation), rebuild both before trusting a dogfood verdict. A rest dist left over from the main re-measure invalidated the first round of ablation dogfood runs.ablation-dist-preflight. Use a code spelling as the marker.bootStack. The signed-in admin is refused export ofsys_audit_log(EXPORT_NOT_PERMITTED). The dogfood fixture grantsallowExportonsys_audit_logto its exporter set. That is a fixture grant; the gate is unchanged.res.end(), so an HTTP test must poll the ledger, bounded.check:docs,check:skill-examples) refuse until spec is rebuilt:pnpm --filter @objectstack/spec build.engine.inserttripscheck-tenant-audit-census. A new durability callee needs bothDURABILITY_CRITICAL_CALLEESand theWRITE_SHAPED_CALLEEScopy (check:swallow-census-controls). A test options object castas anytripscheck:query-options-erasure, which ratchets the test surface.objectstack-fleet commentedon Nov 6, 2026
domain:server#1 · os-elon-musk · session_016WV4cxfxbKV82MKKQzizpP
os-dev-report
{ "issue": 30955, "status": "done", "branch": "claude/project-thread-y3qc8b", "pr": "https://github.com/objectstack-ai/objectstack/pull/31000", "session": "session_016WV4cxfxbKV82MKKQzizpP", "premise_still_valid": true, "summary": "Re-measured on main 1b3e22f02c: sys-audit-log.object.ts:244/:250/:289 still retire `export` and the export door (data-action-routes.ts:1096) writes no ledger row; the dogfood file run against main's source sees 0 export rows after 6 exports. The door now hands the `audit` slot's new `recordDataExport` (plugin-audit data-export-audit.ts) one event per export that cleared its gates and began reading, after res.end(); the writer stores one run-level row (action 'export', record_id null) with format, row count, limit/requested/cap, truncated, a bounded filter summary of field names and operators only, search as a boolean, and outcome (failed: status + code). A refused export (EXPORT_NOT_PERMITTED, 400 parameter/filter refusal, template) writes no row, with the reasoning in the PR body; a lost row is logged at error once per cause and never changes the file. `export` is restored on the enum and the config_changes view, with the ADR-0087/#8147 comments updated in the same diff. Head 63e2ee7f56 (main merged).", "tests": "Final head 63e2ee7f56 unless noted. plugin-audit `pnpm --filter @objectstack/plugin-audit test` exit 0: Test Files 76/76, Tests 1189/1189 (run on d2f3695163; the one later test-file change re-run: data-export-audit.test.ts 15/15, check:test-typecheck exit 0). rest `pnpm --filter @objectstack/rest test` exit 0: Test Files 393/393, Tests 7483 passed, 332 skipped (rest source unchanged since that run). Dogfood `test/data-export-audit-row.dogfood.test.ts` 8/8 on rebuilt dists. typecheck exit 0: plugin-audit, rest, both check:test-typecheck, dogfood (NODE_OPTIONS=--max-old-space-size=6144, as CI). `pnpm lint` exit 0. Main re-measure (source put back to main, tests kept, rebuilt): dogfood 7 failed / 1 passed (0 rows per export), door describe 7 failed / 5 passed; restored, git diff HEAD empty, writer blob == HEAD 307d2c3064. Ablations via scripts/ablation-replace.mjs WRAP, each restored blob == HEAD: A writer dropped from the slot (dist marker `...createDataExportAuditWriter(` present pristine, absent after) unit 6/15 red, dogfood 7/8 red; A' door call dropped, door describe 7/12 red; B1 filter scrub dropped (marker live in dist) unit 1/15 red = the no-cell-value case, dogfood 2/8 red; B2 search scrub dropped, unit 1/15 red = the same case, dogfood 2/8 red (comment marker stripped by esbuild, preflight exit 1; the searched-case red is the dist evidence). A first A/B1 dogfood round ran against a rest dist still built from main and was discarded and re-run after a HEAD rebuild.", "gates": "`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on 63e2ee7f56: 117 commands, each run with its exit code captured before any pipe into ran.list; all 117 exit 0. `--ran ran.list`: exit 0, 117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN. Earlier rounds caught and fixed: check-tenant-audit-census (new write site, census 261 to 262 regenerated + 6 prose figures), sys-audit-log-pointer-pair.test.ts (config_changes filter pin), check:query-options-erasure (a test options `as any` removed), check:swallow-census-controls (WRITE_SHAPED_CALLEES copy gains persistDataExportAuditRow). check-durability-degradation-log-level proven red by downgrading the new callee to warn (exit 1). check-i18n-bundles OK, check:i18n-coverage exit 0, check-nul-bytes OK. CI check-runs on #31000 not read by this thread.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "4 — all reads: issue_read get_comments #30955, pull_request_read get #30937 (body template), list_pull_requests head filter (no existing PR), pull_request_read get #31000 (read-back). No write tool.", "api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write): pr_create #31000 (draft create + assignee os-elon-musk; run 42251615949 success, body read back byte-identical) and this os-dev-report comment via post-stamped.mjs. Plus 8 git pushes to claude/project-thread-y3qc8b only.", "open_questions": [ { "question": "Should an export of an object name the registry does not know (404 OBJECT_NOT_FOUND, reachable only by a caller the export gate let through for that name) write a failed row, as it does now, or no row?", "options": [ "A keep the failed row: the read began and was refused by the protocol, like INVALID_FIELD", "B special-case OBJECT_NOT_FOUND as a refusal with no row (one line in data-action-routes.ts)" ], "recommendation": "A, because it keeps one rule (gates passed and the read began means a row) and the case is only reachable past the export gate; it is probed, not pinned, so B stays cheap if the reviewer prefers it" } ], "security_findings": "none", "out_of_scope_findings": [ "class: c · reach: named producer · ui/packages/plugin-detail/src/HistoryTimeline.tsx comment calls `export` retired; the timeline falls back to an outline badge for it · ⛔ ui/** · dedupe words: HistoryTimeline export retired comment", "class: c · reach: exception: release-text · packages/spec/src/migrations/entries/semantic/17.audit-log-action-enum-retired.ts says no export feature ever wrote an audit row (true for 17.x) · ⛔ packages/spec/** · dedupe words: audit-log-action-enum-retired export restored", "class: c · reach: named producer · docblock writer lists in plugin-audit audit-log-read-visibility.ts and audit-log-field-redaction.ts do not name the export writer; behaviour already covers run-level rows · dedupe words: audit ledger writer list docblock export", "carrier: PR #31000 body (Acceptance notes) · noted, not filed. Census race: #30946 also moves the tenant-audit census 261 to 262; whichever lands second merges main and re-runs node scripts/tenant-audit-census.mjs --write (handoff at /mnt/project-files/server-lane-seat1/30955/handoff.md)" ] }Generated by Claude Code