Skip to content

Data export writes no sys_audit_log row and the export action value is retired, so no export of any object (the audit ledger included) can be audited #30955

Open

Description

@objectstack-fleet

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)

Expected

  • One sys_audit_log row per completed export, for every object (not just the ledger), in the same PR as the restored export enum 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, as plugin-auth's admin-import-users.ts import row does) unless the lane judges otherwise.

Acceptance

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

  1. added
    bugSomething isn't working
    priority:p1High: required for production / M2
    pm:queueReady for the PM dispatch loop
    repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 6, 2026
  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

    Claim: PM loop patrol · 2026-11-06T14:27Z
    Session: session_016WV4cytLmJxfAmgPVjUbbW
    Account: os-elon-musk (the seat's linked user as GET /user answers 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-30955
    Domain: domain:server
    Seat: domain:server#1
    File surface (card Expected and Acceptance, nothing else):

    • packages/rest/src/data-action-routes.ts (the GET /data/:object/export door writes one sys_audit_log row per completed export, run-level, record_id: null) and the audit writer it reaches, following the incumbent run-level writer (plugin-auth's admin-import-users.ts import row); their tests.
    • packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts: restore the export value of action and the view filter at about :244, with the ADR-0087 / audit-log (C): retire export / import / permission_change from the sys_audit_log action 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_PERMITTED stays 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 report
    Container & model: M, mode:card-thread, model: default, effort: xhigh (p1, an audit row beside exported data)
    Clause-②: yes, expected (widening: a served audit action value comes back, with its writer). The dev re-derives it from the diff
    Metadata-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 of GET /data/:object/export
    Thread-read: none
    Premise: as the filer measured on 6ddf827236: sys-audit-log.object.ts:289 retired export (#8147, nothing wrote it), and data-action-routes.ts:1096 writes no sys_audit_log row. The dev re-reads both on current main, and says whether a refused export records nothing or a refusal row, and why
    Owed before landing: a CONTRACT_REVIEW_TIER review (a served enum value restored)
    Serial constraints cleared: an open-PR search for the export door and the audit action finds only #30950 (plugin-auth session audit), which touches none of these files; the dev rebases on a collision

  3. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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 thread session_016WV4cxfxbKV82MKKQzizpP
    Account: os-elon-musk (the seat's linked user as GET /user answers 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-30955
    Domain: domain:server
    Seat: domain:server#1
    File surface (card Expected and Acceptance, nothing else):

    ⛔ 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_PERMITTED stays 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 report
    Container & model: M, mode:card-thread, model: default, effort: xhigh (p1, an audit row beside exported data)
    Clause-②: yes, expected (widening: a served audit action value comes back, with its writer). The dev re-derives it from the diff
    Metadata-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 of GET /data/:object/export
    Thread-read: 6687178882
    Premise: as the filer measured on 6ddf827236: sys-audit-log.object.ts:289 retired export (#8147, nothing wrote it), and data-action-routes.ts:1096 writes no sys_audit_log row. The dev re-reads both on current main, and says whether a refused export records nothing or a refusal row, and why
    Owed before landing: a CONTRACT_REVIEW_TIER review (a served enum value restored)
    Serial constraints cleared: an open-PR search for the export door and the audit action finds only #30950 (plugin-auth session audit), which touches none of these files; the dev rebases on a collision

  4. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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

    • PR: feat(audit): every data export writes one run-level sys_audit_log row, and the export action 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.
    • Branch: claude/project-thread-y3qc8b, head 63e2ee7f56 (9 commits). Main 1b3e22f02c is merged in.
    • Claim: 6687338710 (a full re-post of 6687178882). Seat 1 accepted three additions:
      • the dogfood file;
      • the tenant-audit census regeneration;
      • one WRITE_SHAPED_CALLEES line in scripts/measure-durability-swallow-family.mjs.
    • Body lines: Fixes #30955, Clause-②: yes (widening), Metadata-protocol: none.
    • Changeset: .changeset/30955-data-export-audit-row.md, patch for @objectstack/plugin-audit and @objectstack/rest.

    What the PR does

    • GET /data/:object/export hands one event to the audit slot's recordDataExport for each export that passed its gates and began reading. The event is handed after the response has ended.
    • plugin-audit's data-export-audit.ts writes one run-level sys_audit_log row from it: action: 'export', record_id: null. The metadata carries:
      • format, row count, limit / requested limit / cap, truncated;
      • a filter summary (field names and operators only, bounded at 32 terms);
      • search as a boolean, never the term;
      • outcome, plus status / code when the export failed.
    • No row is written for a refusal: EXPORT_NOT_PERMITTED, a 400 parameter or filter refusal, or the template.
    • export is restored on the enum and on the config_changes view.

    Local verification on the final head

    Ablations and the main re-measure are in the PR body.

    What's left

    1. Contract review. It is owed before landing, at CONTRACT_REVIEW_TIER, because a served enum value is restored. Nobody has done it.
    2. CI on feat(audit): every data export writes one run-level sys_audit_log row, and the export action comes back with its writer #31000. Not read by this thread, and no CI fix rounds have been done.
    3. Open question for the reviewer. An export of an object name the registry does not know currently writes an 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 in data-action-routes.ts. This behaviour was probed, not pinned.
    4. Noted, not edited (outside the surface):
      • ui/packages/plugin-detail/src/HistoryTimeline.tsx has a comment calling export retired.
      • The spec migration entry 17.audit-log-action-enum-retired.ts (17.x history) is spec ⛔.
      • The writer lists in the docblocks of audit-log-read-visibility.ts and audit-log-field-redaction.ts don't name the writer.
      • Exported columns are not recorded; that is a possible follow-up.

    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:

    1. Merge main.
    2. Run node scripts/tenant-audit-census.mjs --write. It should then read 263, plus whatever else landed.
    3. Re-check the hand-written figures on content/docs/permissions/tenant-audit-census.mdx, which the gate holds to the census: write call sites, decidable, decidably elevated, and the 2 / N citation. Then run node scripts/check-tenant-audit-census.mjs until it is OK.
    4. Push.

    Traps met on this card

    • Dogfood reads dists. The dogfood suite resolves @objectstack/rest and @objectstack/plugin-audit through 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 imports. If you remove the only use of an import in an ablation, tsup's DTS step fails (TS6133). Replace it with a type-only use instead, for example an empty object cast to a type that names the import.
    • Comment markers. esbuild strips comments, so a comment marker never shows in ablation-dist-preflight. Use a code spelling as the marker.
    • Admin cannot export the ledger on bootStack. The signed-in admin is refused export of sys_audit_log (EXPORT_NOT_PERMITTED). The dogfood fixture grants allowExport on sys_audit_log to its exporter set. That is a fixture grant; the gate is unchanged.
    • The row lands after the file. It is written after res.end(), so an HTTP test must poll the ledger, bounded.
    • Stale spec artefacts after merging main. The spec gates (check:docs, check:skill-examples) refuse until spec is rebuilt: pnpm --filter @objectstack/spec build.
    • New write sites and callees trip three gates. A new engine.insert trips check-tenant-audit-census. A new durability callee needs both DURABILITY_CRITICAL_CALLEES and the WRITE_SHAPED_CALLEES copy (check:swallow-census-controls). A test options object cast as any trips check:query-options-erasure, which ratchets the test surface.
  5. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p1High: required for production / M2repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions