You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
security(plugin-audit): sys_audit_log copies already moved to an archive datasource may still hold pre-#24688 mail bodies in clear — measure and decide #24899
Those bodies can carry reset, verification, magic-link and invitation links. Most will have expired, but not necessarily all of them, and an invitation may still be open.
Measure
Which deployments have an archive datasource configured, and who can read it (doors, roles)?
Is there a supported way to run the mask over the archive? For example, the same runner pointed at the archive engine.
Then decide: extend the command, document a manual step, or accept the risk with a stated expiry argument. Owe a 17.x note if a 17.x line is cut.
Claim: epic seat round · 2026-10-20T14:59Z · dispatched under the maintainer's standing direction on security findings, 「24625 这种你直接派发处理」
Session: session_01Y5583bs2RDd53MH6kM1ca3
Account: os-steve (the card's assignee)
Branch: claude/issue-24899-archived-mail-bodies
Worktree: the cloud session's own checkout (mode:cloud)
Domain: domain:services
Seat: epic:#15194 (the epic lane; this card is a residual of #24722, which the epic seat dispatched)
File surface: packages/cli/src/commands/migrate/audit-mail-bodies.ts and its tests; the runner it calls (wherever #24722 put it, e.g. packages/plugins/plugin-audit/src/**) and its tests; content/docs/deployment/cli.mdx (that command's section only); one .changeset/*.md. Read-only: packages/objectql/src/lifecycle/** and the datasource wiring. ⛔ Not the lifecycle policy (sys-audit-log.object.tslifecycle.archive), ⛔ not docs/adr/**, ⛔ not .github/workflows/**.
Container & model: M, mode:cloud, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate)
Clause-②: no (expected: an operator command reaches one more datasource; the dev re-declares it from the diff)
Responsibility: the code that produces the risk is LifecycleService archiving sys_audit_log (archive: { after: '90d', to: 'archive', keep: '7y' }), together with #24722's os migrate audit-mail-bodies, which masks the hot engine only | covered by no platform path today | reached by whoever can read the archive datasource, on deployments that configure one; a measured fact, not yet known
Thread-read: none
Serial constraints cleared: none. A scan of every open PR's files finds only the Version Packages PR #24876 touching this area (it consumes .changeset/24693-audit-mail-bodies.md).
{
"issue": 24899,
"status": "done",
"branch": "claude/issue-24899-archived-mail-bodies",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24934",
"session": "session_01WQWCrorWxW5zDh4n6jCEF5",
"premise_still_valid": true,
"summary": "Measured on origin/main 81e6edd46. The Archiver copies sys_audit_log rows to the archive datasource verbatim (lifecycle-service.ts:1471-1520: hot.find then cold.upsert then hot.bulkDelete, no writer, no mask), so pre-#24688 copies sit there in clear and young clear hot copies would be archived in clear. sys_activity declares no archive (telemetry, 14d rotation). Route (a): plugin-audit gains migrateArchivedMailLogBodyCopies (the same pages and planners through the datasource's driver, preserveAudit, a never-created archive table reads as no rows) and an objects option. os migrate audit-mail-bodies reads each copy table's declared lifecycle.archive.to and masks it through engine.getDriverByName(to). It boots with composeDeclaredDatasources (an archive is declared with autoConnect: true, ADR-0062 D2(c)), reports not-connected and exits 1 when an active runtime datasource row of that name exists but this boot cannot connect it, and ends with exitOneShotCommand (without it the declared pool kept the process alive, measured). Masking hot before the next sweep covers the future-archived rows: pinned end to end with the real Archiver. Clause-② re-declared from the diff: yes (widening), because the --json document gains an archive key and plugin-audit gains exports; minor changeset for both. One surface question: check:tenant-audit-census goes 232 to 233 on the new driver.update site; its regeneration writes content/docs/permissions/tenant-audit-census.mdx and docs/audits files outside the surface, so it is not done here and that required gate stays red until the seat widens the surface or regenerates.",
"tests": "head 39fc914f (diff since 8a87750c is one test-file header comment). plugin-audit: pnpm test 983/983 (60 files), typecheck green; the new runner cases run over a real SqliteWasmDriver provisioned as the Archiver does (syncSchema + upsert): dry run, apply, idempotent re-run, an archive with no table yet, a refused read. cli: build + typecheck green; vitest --project unit 4634/4635, the one failure test/hook-timeout-override-refusal.test.ts CONTROL timed out at 5000ms beside the gate sweep, 4/4 re-run alone, a spawn this diff does not touch. cli integration, run locally because the diff touches that layer: audit-mail-bodies.archive.integration.test.ts 7/7 (53.1s); data-commands.absent-database + schema-migrate.one-shot-family 151/151; test/json-stdout-purity.e2e.test.ts declared to CI. End to end, counts only: the real Archiver moved 3 rows, 2 with a clear body; dry run hot to-mask 1, archive examined scanned 2 to-mask 2, nothing written; --apply archive rewritten 2, clear 0 in both stores, every other archived column equal including updated_at; re-run rewritten 0, rows unchanged; the young copy aged and archived by a second sweep landed masked (archive rows 4, clear 0); sentinel printed by no run; runtime-row archive not-connected exit 1; no archive none exit 0. Ablation via scripts/ablation-replace.mjs (archive lookup to undefined): 4 failed / 3 passed, restored to HEAD blob b6e1355a; CLI runs from source (bin/run-dev.js), so no dist leg.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands: 101 derived, all run, exit captured before any pipe; --ran reconciles 101 derived / 97 run / 4 NOT-MEASURED / 0 unrun. exit 1: node scripts/check-tenant-audit-census.mjs and its --self-test (233 vs committed 232; green at base 81e6edd46: the surface question above). exit 3 NOT MEASURED, left to CI: check-plugin-teardown-shape --self-test (shallow clone), spec check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage (each needs the full build). Every other family exit 0. node scripts/pm/check-governed-merges.mjs --branch origin/claude/issue-24899-archived-mail-bodies: exit 0, NOT governed, 791 changed lines. eslint --no-inline-config on the changed files: exit 0. PR check-runs: in_progress at report time, not awaited.",
"mcp_calls": "5 — mcp__github__issue_read x2 (get, get_comments) plus 3 ToolSearch loads; no MCP write tool",
"api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs (repository_dispatch, executed as objectstack-fleet[bot]): pr_create (POST /repos/objectstack-ai/objectstack/pulls, POST .../issues/24934/assignees) and this comment (POST .../issues/24899/comments)",
"open_questions": [
{
"question": "Surface: check:tenant-audit-census (required, Lint and Repo Gates) reds on the new archive write call site, 232 to 233. Who regenerates content/docs/permissions/tenant-audit-census.mdx and the docs/audits/2026-08-tenant-audit-* counts and prose?",
"options": [
"A widen this card's surface to those census artefacts and the dev regenerates them on this branch (node scripts/tenant-audit-census.mjs --write, plus the hand-written counts it names)",
"B the seat regenerates them on this branch"
],
"recommendation": "A, because it is a mechanical regeneration of artefacts this diff alone moved"
},
{
"question": "packages/cli/src/utils/schema-migrate.ts:1176-1178 lists the callers of composeDeclaredDatasources and ends 'by nothing else'; os migrate audit-mail-bodies is now one. That file is outside the surface. Fold a one-line comment edit into this PR?",
"options": [
"A widen the surface by that one comment line",
"B leave it for the next PR that touches schema-migrate.ts"
],
"recommendation": "A, because the comment is now one caller short"
}
],
"out_of_scope_findings": [
"carrier: the 17.x note — the writer mask (#24688) and this command shipped in 18.0.0; a 17.x line, if cut, has neither, so it owes the whole mail-body note (hot and archive), not only the archive half · noted, not filed",
"carrier: none — an archive registered only by a host plugin publishing driver.archive that the CLI boot does not compose reads none unless a runtime datasource row of that name exists; recorded in the PR acceptance notes · noted, not filed"
]
}
Claim amendment · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-20T16:35Z · amends claim 6293910100, answering the surface question in os-dev-report 6295884727.
File surface: widened by exactly the census regeneration.
The new archive runner adds one driver.update site against sys_audit_log. The required check:tenant-audit-census therefore drifts from 232 to 233.
.github/workflows/lint.yml names one repair path for that gate, and it is mechanical: node scripts/tenant-audit-census.mjs --write.
Its outputs are added to the surface:
content/docs/permissions/tenant-audit-census.mdx
the docs/audits/2026-08-tenant-audit-* files that the generator rewrites
Neither is a governed path. ⛔ Write them by the generator only, never by hand.
Not widened: the stale comment at packages/cli/src/utils/schema-migrate.ts:1176-1178. It is not this card's code path. List it in the PR's Acceptance notes as out of scope; the seat files it if it needs a card.
Census prose figures: allowed, number-only.scripts/check-tenant-audit-census.mjs check B (PROSE) holds 10 hand-written figures in content/docs/permissions/tenant-audit-census.mdx, outside its generated region. That header says they are hand-written by design. Each one is a +1 on a number this diff moved: 232→233, 50→51, 58→59, 76→77 and 93→94. The dev may edit exactly those figures and nothing else in the prose. The proof is both check-tenant-audit-census invocations exiting 0.
packages/cli/src/utils/schema-migrate.ts:1176-1178: widened after all. That comment lists the callers of composeDeclaredDatasources. This diff adds one (os migrate audit-mail-bodies), so the change itself makes the comment false, and the fix belongs in this PR. Comment text only. This replaces amendment 1's "not widened".
The new site's census posture stays as the generator classifies it:update, the object chosen at run time, tenancy undecidable, context options unreadable. That is honest for a driver-level write to the archive datasource, which no tenant scope covers and which masks every organization's archived copies by design. The optional literal-options rewrite is not asked for.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: epic seat round · 2026-10-20T14:59Z · dispatched under the maintainer's standing direction on security findings, 「24625 这种你直接派发处理」
Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steve(the card's assignee)Branch:
claude/issue-24899-archived-mail-bodiesWorktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194(the epic lane; this card is a residual of #24722, which the epic seat dispatched)File surface:
packages/cli/src/commands/migrate/audit-mail-bodies.tsand its tests; the runner it calls (wherever #24722 put it, e.g.packages/plugins/plugin-audit/src/**) and its tests;content/docs/deployment/cli.mdx(that command's section only); one.changeset/*.md. Read-only:packages/objectql/src/lifecycle/**and the datasource wiring. ⛔ Not the lifecycle policy (sys-audit-log.object.tslifecycle.archive), ⛔ notdocs/adr/**, ⛔ not.github/workflows/**.Container & model:
M,mode:cloud,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate)Clause-②: no (expected: an operator command reaches one more datasource; the dev re-declares it from the diff)
Responsibility: the code that produces the risk is
LifecycleServicearchivingsys_audit_log(archive: { after: '90d', to: 'archive', keep: '7y' }), together with #24722'sos migrate audit-mail-bodies, which masks the hot engine only | covered by no platform path today | reached by whoever can read thearchivedatasource, on deployments that configure one; a measured fact, not yet knownThread-read: none
Serial constraints cleared: none. A scan of every open PR's files finds only the Version Packages PR #24876 touching this area (it consumes
.changeset/24693-audit-mail-bodies.md).objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24899, "status": "done", "branch": "claude/issue-24899-archived-mail-bodies", "pr": "https://github.com/objectstack-ai/objectstack/pull/24934", "session": "session_01WQWCrorWxW5zDh4n6jCEF5", "premise_still_valid": true, "summary": "Measured on origin/main 81e6edd46. The Archiver copies sys_audit_log rows to the archive datasource verbatim (lifecycle-service.ts:1471-1520: hot.find then cold.upsert then hot.bulkDelete, no writer, no mask), so pre-#24688 copies sit there in clear and young clear hot copies would be archived in clear. sys_activity declares no archive (telemetry, 14d rotation). Route (a): plugin-audit gains migrateArchivedMailLogBodyCopies (the same pages and planners through the datasource's driver, preserveAudit, a never-created archive table reads as no rows) and an objects option. os migrate audit-mail-bodies reads each copy table's declared lifecycle.archive.to and masks it through engine.getDriverByName(to). It boots with composeDeclaredDatasources (an archive is declared with autoConnect: true, ADR-0062 D2(c)), reports not-connected and exits 1 when an active runtime datasource row of that name exists but this boot cannot connect it, and ends with exitOneShotCommand (without it the declared pool kept the process alive, measured). Masking hot before the next sweep covers the future-archived rows: pinned end to end with the real Archiver. Clause-② re-declared from the diff: yes (widening), because the --json document gains an archive key and plugin-audit gains exports; minor changeset for both. One surface question: check:tenant-audit-census goes 232 to 233 on the new driver.update site; its regeneration writes content/docs/permissions/tenant-audit-census.mdx and docs/audits files outside the surface, so it is not done here and that required gate stays red until the seat widens the surface or regenerates.", "tests": "head 39fc914f (diff since 8a87750c is one test-file header comment). plugin-audit: pnpm test 983/983 (60 files), typecheck green; the new runner cases run over a real SqliteWasmDriver provisioned as the Archiver does (syncSchema + upsert): dry run, apply, idempotent re-run, an archive with no table yet, a refused read. cli: build + typecheck green; vitest --project unit 4634/4635, the one failure test/hook-timeout-override-refusal.test.ts CONTROL timed out at 5000ms beside the gate sweep, 4/4 re-run alone, a spawn this diff does not touch. cli integration, run locally because the diff touches that layer: audit-mail-bodies.archive.integration.test.ts 7/7 (53.1s); data-commands.absent-database + schema-migrate.one-shot-family 151/151; test/json-stdout-purity.e2e.test.ts declared to CI. End to end, counts only: the real Archiver moved 3 rows, 2 with a clear body; dry run hot to-mask 1, archive examined scanned 2 to-mask 2, nothing written; --apply archive rewritten 2, clear 0 in both stores, every other archived column equal including updated_at; re-run rewritten 0, rows unchanged; the young copy aged and archived by a second sweep landed masked (archive rows 4, clear 0); sentinel printed by no run; runtime-row archive not-connected exit 1; no archive none exit 0. Ablation via scripts/ablation-replace.mjs (archive lookup to undefined): 4 failed / 3 passed, restored to HEAD blob b6e1355a; CLI runs from source (bin/run-dev.js), so no dist leg.", "gates": "node scripts/pm/dispatch-gates.mjs --commands: 101 derived, all run, exit captured before any pipe; --ran reconciles 101 derived / 97 run / 4 NOT-MEASURED / 0 unrun. exit 1: node scripts/check-tenant-audit-census.mjs and its --self-test (233 vs committed 232; green at base 81e6edd46: the surface question above). exit 3 NOT MEASURED, left to CI: check-plugin-teardown-shape --self-test (shallow clone), spec check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage (each needs the full build). Every other family exit 0. node scripts/pm/check-governed-merges.mjs --branch origin/claude/issue-24899-archived-mail-bodies: exit 0, NOT governed, 791 changed lines. eslint --no-inline-config on the changed files: exit 0. PR check-runs: in_progress at report time, not awaited.", "mcp_calls": "5 — mcp__github__issue_read x2 (get, get_comments) plus 3 ToolSearch loads; no MCP write tool", "api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs (repository_dispatch, executed as objectstack-fleet[bot]): pr_create (POST /repos/objectstack-ai/objectstack/pulls, POST .../issues/24934/assignees) and this comment (POST .../issues/24899/comments)", "open_questions": [ { "question": "Surface: check:tenant-audit-census (required, Lint and Repo Gates) reds on the new archive write call site, 232 to 233. Who regenerates content/docs/permissions/tenant-audit-census.mdx and the docs/audits/2026-08-tenant-audit-* counts and prose?", "options": [ "A widen this card's surface to those census artefacts and the dev regenerates them on this branch (node scripts/tenant-audit-census.mjs --write, plus the hand-written counts it names)", "B the seat regenerates them on this branch" ], "recommendation": "A, because it is a mechanical regeneration of artefacts this diff alone moved" }, { "question": "packages/cli/src/utils/schema-migrate.ts:1176-1178 lists the callers of composeDeclaredDatasources and ends 'by nothing else'; os migrate audit-mail-bodies is now one. That file is outside the surface. Fold a one-line comment edit into this PR?", "options": [ "A widen the surface by that one comment line", "B leave it for the next PR that touches schema-migrate.ts" ], "recommendation": "A, because the comment is now one caller short" } ], "out_of_scope_findings": [ "carrier: the 17.x note — the writer mask (#24688) and this command shipped in 18.0.0; a 17.x line, if cut, has neither, so it owes the whole mail-body note (hot and archive), not only the archive half · noted, not filed", "carrier: none — an archive registered only by a host plugin publishing driver.archive that the CLI boot does not compose reads none unless a runtime datasource row of that name exists; recorded in the PR acceptance notes · noted, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim amendment · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-20T16:35Z · amends claim 6293910100, answering the surface question in os-dev-report 6295884727.File surface: widened by exactly the census regeneration.
driver.updatesite againstsys_audit_log. The requiredcheck:tenant-audit-censustherefore drifts from 232 to 233..github/workflows/lint.ymlnames one repair path for that gate, and it is mechanical:node scripts/tenant-audit-census.mjs --write.content/docs/permissions/tenant-audit-census.mdxdocs/audits/2026-08-tenant-audit-*files that the generator rewritesNot widened: the stale comment at
packages/cli/src/utils/schema-migrate.ts:1176-1178. It is not this card's code path. List it in the PR's Acceptance notes as out of scope; the seat files it if it needs a card.objectstack-fleet commentedon Oct 20, 2026
Claim amendment 2 · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-20T16:39Z · amends 6295968920.scripts/check-tenant-audit-census.mjscheck B (PROSE) holds 10 hand-written figures incontent/docs/permissions/tenant-audit-census.mdx, outside its generated region. That header says they are hand-written by design. Each one is a +1 on a number this diff moved: 232→233, 50→51, 58→59, 76→77 and 93→94. The dev may edit exactly those figures and nothing else in the prose. The proof is bothcheck-tenant-audit-censusinvocations exiting 0.packages/cli/src/utils/schema-migrate.ts:1176-1178: widened after all. That comment lists the callers ofcomposeDeclaredDatasources. This diff adds one (os migrate audit-mail-bodies), so the change itself makes the comment false, and the fix belongs in this PR. Comment text only. This replaces amendment 1's "not widened".update, the object chosen at run time, tenancyundecidable, contextoptions unreadable. That is honest for a driver-level write to the archive datasource, which no tenant scope covers and which masks every organization's archived copies by design. The optional literal-options rewrite is not asked for.objectstack-fleet commentedon Oct 20, 2026
Seat ACCEPT pointer · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-20T17:57Z. ACCEPT → PR #24934 comment 6297547716. The PR is readied and auto-merge is armed;Fixes #24899closes this card on merge.