Repository navigation
feat(cli,plugin-audit): os migrate audit-mail-bodies masks the archive datasource too - #24934
Conversation
…rce's driver too Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
…e datasource too The Archiver copies sys_audit_log rows past the hot window to the archive datasource verbatim, so copies archived before the writer masked kept the mail body in clear out of the hot run's reach. The command now runs the same mask through the declared archive target's driver, and refuses to report a runtime archive datasource it cannot connect as clean. Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 35 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 35 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 32c016a1ec3832a25423d5beb542b35c73da9547 && git checkout 32c016a1ec3832a25423d5beb542b35c73da9547
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 71696121a64d8bd057fac969a72a84295890ed37 791db71c37806719de5ba66281186385cc0a0100 && git checkout -B drift-repro 71696121a64d8bd057fac969a72a84295890ed37 && git merge --no-ff 791db71c37806719de5ba66281186385cc0a0100
node scripts/docs-audit/affected-docs.mjs --json 71696121a64d8bd057fac969a72a84295890ed37
|
…'s write site Generator output only (node scripts/tenant-audit-census.mjs --write). Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
…urces caller list to this diff The ten hand-written census figures this diff moved, each +1, and the caller list that os migrate audit-mail-bodies now joins. Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
…ces' caller list The previous commit's message names this change; it landed here. Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: This is a read-only review of the head, read in a detached worktree that was removed afterwards. It covers the full diff from merge-base ① Derived judgments1. Archive mask correctness
2. The CLI
3. The future-archived rows claim: true.
4. Tests
5. Docs and changeset
6. Census
7. ② Semver level
Seat point (1): the boot behaviour change.
③ Boundary flagsB1 (BLOCKING): an under-mask reported clean, and false docs sentences. A declared archive that this boot does not connect reads
B2 (BLOCKING): a false docs sentence, and the operator is not told (seat point 2). A host-registered archive reads
B3 (non-blocking, owed in the same pass): the changeset omits the boot consequence (seat point 1). Add one line: a bound, mapped or boot-critical declared datasource that cannot connect now fails the command's boot with exit 1, as on B4 (non-blocking, pre-existing): a contradicted sentence next to the new text. cli.mdx:1503-1506 says the copies "age out with each table's retention (... 90 for the audit log)". The object says rows are retained hot when there is no archive (sys-audit-log.object.ts:32-35), and moved for 7 years when there is one. The new B5 (non-blocking): case 1's dry-run pin is vacuous. It asserts on the pre-run snapshot (test:244-246, :297). Snapshot both stores after B6 (non-blocking): a concurrent-sweep race.
B7 (open question): the archive target ignores B8 (non-blocking): the integration test is not strictly counts and booleans only. A few CI on the head (read at 16:53Z). No check-run is red, so no owner is to blame.
Implemented-by: VERDICT: FAIL |
…nected Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQWCrorWxW5zDh4n6jCEF5 Co-authored-by: Claude <noreply@anthropic.com>
Contract review (delta re-review after FAIL at 4fb9afa)Served-tier: This is a read-only, in-seat review by the epic seat of the delta ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
Seat ACCEPT · epic PM Seat check
|
… into 7b-0 One conflict: service-messaging notification-display-title.test.ts keeps main's removal of the retired NotificationSubscription import and this branch's Default Organization testkit import. Claude-Session: https://claude.ai/code/session_018EszgtA3V2KXBX5tf59Hi4 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #24899
Clause-②: yes (widening)
os migrate audit-mail-bodiesnow masks the mail bodies in thesys_audit_logcopies the lifecycle Archiver already moved to thearchivedatasource. Hot copies masked by the run are archived masked later, so the same run covers those too. The command keeps its dry-run /--apply/--yes/--jsonshape, and its report is still counts only.Measured
Every reading below is from
origin/mainat81e6edd46and is a count, a status or a boolean. No body value was printed.archivedatasource, and who reads it.archive.toby driver name (packages/objectql/src/lifecycle/lifecycle-service.ts:1344,engine.datasource(name)→packages/objectql/src/engine.ts:21671, thedriversmap). A driver is registered under the namearchivein one of three ways:datasourcesentry. Nothing routes objects to an archive, so the ADR-0062 D2 gate connects it only withautoConnect: true(packages/services/service-datasource/src/datasource-connection-service.ts:301-305,packages/spec/src/data/datasource.zod.ts:665).sys_metadatarow of typedatasource, created in Setup → Datasources and rehydrated byos serveat boot (packages/services/service-datasource/src/datasource-admin-plugin.ts:125-139,:734-760).driver.archiveservice (packages/objectql/src/plugin.ts:1060-1076).name: 'archive'datasources underexamples/andpackages/create-objectstack. Every deployment that declares none keeps every row hot:archive-pending(lifecycle-service.ts:1349-1355).engine.datasource(has one caller,lifecycle-service.ts:1344, and the engine never routessys_audit_logthere. Otherwise there is direct database access, and the datasource admin door:GET /datasources/:name/remote-tablesplus the object-draft flow, gated byrequireDatasourceAdmin(packages/services/service-datasource/src/admin-routes.ts:706-707). A federated object an admin binds over the archived table would serve it over REST.hot.find→cold.upsert(object, row, ['id'])→hot.bulkDelete(lifecycle-service.ts:1471-1520), with no hook, no writer and no mask. So a hot copy written before fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688 and archived before the operator runs the mask lands in the archive in clear. Pinned below in case 4: a copy masked hot is archived masked.packages/plugins/plugin-audit/src/mail-log-body-migration.ts,migrateMailLogBodyCopies). Two facts made one sound:IDataDrivercontract (findwithwhere/fields/orderBy/limit, andupdate(object, id, data, { preserveAudit })).bootSchemaStackalready connects declared datasources when asked to (composeDeclaredDatasources,packages/cli/src/utils/schema-migrate.ts:1345-1350).sys_activity: no archive. Its lifecycle istelemetry, with 14-day retention and rotation (packages/plugins/plugin-audit/src/objects/sys-activity.object.ts:29-34), so its copies are only ever hot.What changed
@objectstack/plugin-audit.migrateArchivedMailLogBodyCopies(driver, logger, options)runs the same pages and planners through a datasource's driver, as the Archiver itself does.preserveAuditkeeps each row'supdated_at.isMissingTableError. Any other refused read is a failure.objectsoption on both runners. The engine runner's behaviour is unchanged.os migrate audit-mail-bodies.lifecycle.archive.toand runs the archive runner overengine.getDriverByName(to).composeDeclaredDatasources: true, so a declared archive is connected asos serveconnects it.not-connectedand the command exits1. It does not report a clean archive.exitOneShotCommand, asplan/apply/unmapped-columnsdo. Without it, the pool the declared connection opened kept the process alive after "Graceful shutdown complete". Measured here: it hung, andTaskStopended it.content/docs/deployment/cli.mdx, plus its row in the command table..changeset/24899-archived-mail-bodies.md(minorfor both packages).Proof
packages/cli/src/commands/migrate/audit-mail-bodies.archive.integration.test.ts, 7/7.archiveSQLite datasource (autoConnect: true). A served-shape boot seeds 18.0-writer copies, and the real Archiver (lifecycle.sweep()) moves 3 rows. 2 of them hold a clear body.examined, scanned 2, to-mask 2; nothing written.--apply: archive rewritten 2; clear copies 0 in either store; every other archived column equal,updated_atincluded.not-connected, exit 1. No archive at all:none, exit 0.const driver = undefined) turns cases 1–4 red (4 failed, 3 passed). Restored to the HEAD blobb6e1355a.SqliteWasmDriverstore provisioned as the Archiver does (syncSchema+upsert): dry run, apply, idempotent re-run, an archive with no table yet, a refused read.Local verification (head
39fc914f; the diff since8a87750cis one test-file header comment)@objectstack/plugin-audit:pnpm test983/983 (60 files);typecheckgreen.@objectstack/cli: build +typecheckgreen. Theunitproject: 4634/4635. The one failure wastest/hook-timeout-override-refusal.test.tsCONTROL, a 5000 ms timeout while the gate sweep ran beside it. Re-run alone: 4/4. It spawns a process this diff does not touch.@objectstack/cliintegration, run locally because this diff touches that layer: the new archive pin 7/7;data-commands.absent-database+schema-migrate.one-shot-family151/151.test/json-stdout-purity.e2e.test.tsis left to CI.eslint --no-inline-configclean.node scripts/pm/dispatch-gates.mjs --commands: 101 derived, every one run, exit codes captured before any pipe;--ranreconciles 101 / 97 run / 4 NOT MEASURED / 0 unrun.39fc914f. It is exit 0 on4fb9afa1.driver.update), so the count goes 232 → 233.3340a4adrannode scripts/tenant-audit-census.mjs --write.105a8110and4fb9afa1made the 11 hand-written prose figures +1, numbers only.4fb9afa1,node scripts/check-tenant-audit-census.mjs --self-testpasses 27 cases, and the plain run reports 233 write call sites certified and 23 prose figures held. Both exit 0.packages/plugins/plugin-audit/src/mail-log-body-migration.ts·update· object chosen at run time (the CLI limits the runner tosys_audit_log) · tenancyundecidable· contextoptions unreadable· 1. The write goes to the archive datasource through its own driver, which no tenant scope covers. It masks every organization's archived copies by design, and only the operator command reaches it.check-plugin-teardown-shape --self-test(shallow clone),spec check:skill-examples,check:dual-build-cjs-loads,check:i18n-coverage(each needs the full monorepo build).node scripts/pm/check-governed-merges.mjs --branch origin/claude/issue-24899-archived-mail-bodies: NOT governed; 791 changed lines.Surface question (answered)
The seat widened the card's surface to the census regeneration and its 11 prose figures (6295968920, 6296052814 on #24899, plus the line-210 figure by message). The census is regenerated by the generator only, and
Lint & Repo Gates' census step is green on4fb9afa1.Acceptance notes
Behaviour change of the boot: the command now connects the datasources the config declares, as
os servedoes. A declared datasource that fails to connect therefore fails the command's boot, as it failsos serve's. Before, the command never tried to connect it.Stale comment, fixed here (
f72911d8, amendment 6296052814):packages/cli/src/utils/schema-migrate.ts:1176-1179now namesos migrate audit-mail-bodiesamong the callers ofcomposeDeclaredDatasources. This diff had made the old text false.Not covered by this command: an archive registered only by a host plugin (
driver.archive) that the CLI boot does not compose. It readsnoneunless a runtime datasource row of that name exists. A code-declared archive withoutautoConnect: trueis never connected byos serveeither, so the Archiver never archived to it.17.x: the writer mask (fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688) and this command shipped in 18.0.0. A 17.x line, if cut, has neither, so it would owe the whole mail-body note (hot and archive), not only this half.
Generated by Claude Code
Census, surface and stale-comment lines updated by the epic seat on 2026-10-20 (about 16:50Z) to match head
4fb9afa1.