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): mask the mail bodies already copied into sys_audit_log / sys_activity before #24688 — a cleanup, and the precondition for giving organization admins their own mail bodies back #24693
Pins: an 18.0-shaped fixture with pre-fix mirror rows. After the cleanup, 0 rows hold a clear body; non-body fields are untouched; re-running is a no-op. Reverse-verify.
Claim: epic seat round · 2026-10-20T00:37Z
Session: session_01Y5583bs2RDd53MH6kM1ca3
Account: os-steve (the card's assignee)
Branch: claude/issue-24693-mirror-body-cleanup
Worktree: the cloud session's own checkout (mode:cloud)
Domain: domain:services
Seat: epic:#15194 (the epic lane)
File surface, read on origin/main at or after d1173825b (#24688 merged). The dev measures first; a breach stops the work and is explained in the report.
The existing os migrate audit-metadata-bodies command, its CLI wiring and its tests: the pattern this cleanup follows.
{
"issue": 24693,
"status": "done",
"branch": "claude/issue-24693-mirror-body-cleanup",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24722",
"session": "session_01VHugC2zLtDQFNVzioMiUBo",
"premise_still_valid": true,
"summary": "Measured first: its own command, not a ceremony step. The cleanup masks every copy of both mail logs, so it needs no identity selector and is order-independent of #24681 (the ceremony re-points activity copies to sys_identity_email, which is masked the same, and leaves sys_audit_log as written). It reaches every database a pre-#24688 writer wrote to, and follows the audit-metadata-bodies precedent. Built os migrate audit-mail-bodies (cli) over plugin-audit's migrateMailLogBodyCopies: two pure planners, keyset paging, and a write of the copy column alone under preserveAudit with the stored updated_at. The report is counts only. The writer's inline mask loop became maskMailLogBodyCopy, the one helper both share; #24688's MAIL_LOG_OBJECTS / MAIL_LOG_BODY_FIELDS / SECRET_MASK are reused, not re-spelled. On the 18.0 fixture (base d1173825b): 2 audit + 2 activity copies held a clear body before, 0 after, every other byte identical, and a re-run is a no-op. Option-B precondition recorded below.",
"tests": "At 1f2b0cb7/9b67ec01, then the merged head 6da0543a. plugin-audit full: 59 files, 953 passed (959 after the main merge). cli unit tier: 314 files, 4643 passed. cli integration on the touched files (absent-database, one-shot family): 2 files, 151 passed. json-stdout-purity.e2e (nightly): 64/65; the 1 red is organization-ownership's stderr case, a command this diff does not touch, NOT MEASURED on base. New pins: mail-log-body-migration.test.ts 13/13; audit-mail-bodies.dogfood.test.ts 4/4 (real binary over the 18.0 fixture; budget declared 20 s, measured 12.84 s). Ablation via ablation-replace.mjs (fix committed first): the planner's mask call made a no-op, OS_SKIP_DTS rebuild, dist preflight marker present in 2 built files; unit 7 failed/13, dogfood 2 failed/4 (dry-run count, 0-clear). Restored blob == HEAD, full rebuild, --absent preflight clean, tree clean, then 13/13 and 4/4. R1: pnpm dev --fresh on a random port, health 200, admin sign-in 200, 0 error lines, stopped. R2+R3: upgrade-18x-harness + identity-mail-read-gate + new pin, 3 files, 32 passed (pre- and post-merge). R4: serve-walled-posture-refusal 5 passed. typecheck exit 0 for plugin-audit, cli, dogfood.",
"gates": "dispatch-gates --commands derived 104 (105 after the census files joined). Exit captured before any pipe: 98 exit 0. Fixed, then exit 0: check-tenant-audit-census + --self-test (the runner's one new write call site; regenerated with --write, page figures 230->231, 74->75, 129->130), check:objectql-double-limit (the fake find now applies the bound by presence). NOT MEASURED, exit 3 prerequisite: check-plugin-teardown-shape --self-test (shallow clone), spec check:skill-examples and check:dual-build-cjs-loads (need a full-workspace build). check:merge-driver exit 0; check:engine-double-contract exit 0; check-nul-bytes exit 0; --ran reconciled 105/105. check-governed-merges --branch: 0/13 paths governed, 1130 changed lines. PR check-runs at report time: 'No other open PR may claim the same issue' success; Lint/Type Check/Check Changeset/Governed Surface Queue Guard/Test Core in_progress.",
"mcp_calls": "5 — issue_read x2 (card, comments), pull_request_read x3 (PR 24681 get, PR 24688 comments, PR 24722 check runs); no write tool",
"api_writes": "2 — fleet-write relay: pr_create (POST repos/objectstack-ai/objectstack/pulls + assignees, read back identical); comment on issue 24693 (this report). Plus git push (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the upgrade-harness fixture owner (#24522 lane) · noted, not filed · the fixture dump (sqlite-text-dump.ts) carries tables and indexes only, so the sys_activity rotation VIEW is absent and any read-only command boot over the fixture reads no activity rows; the new pin restores the view and says so",
"carrier: none · noted, not filed · json-stdout-purity.e2e 'os migrate organization-ownership --json still shows every boot diagnostic on stderr' red on this tree (nightly tier); command untouched by this diff; base not measured"
]
}
Precondition for option B (organization administrators read their own non-identity mail bodies again)
Recorded by the os-dev of this card. Option B means relaxing the sys_email body field gate (requiredPermissions: ['manage_platform_settings'] on body_text / body_html) so that an organization administrator reads the bodies of their own organization's non-identity mail. It is admissible only when ALL of the following hold, on the deployment and in the code:
The ledger cleanup has run.os migrate audit-mail-bodies --apply (this card's PR) has run on the deployment and a re-run reports rewritten: 0. Reason: the mirror read redaction serves a sys_audit_log / sys_activity copy the fields the data plane serves for its parent. Relax the sys_email body gate while pre-fix copies still hold bodies in clear, and those copies re-open to organization administrators, including the identity mail written into sys_email before the ceremony. The audit log keeps object_name = 'sys_email' for those rows: the ceremony does not rewrite the ledger.
The identity selector is ONE exported definition. Today it is the where of the identity-mail-move row of SIBLING_MOVES in packages/cli/src/utils/organization-ownership-apply.ts (PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681, read on head edb2db1e8). That is raw SQL evaluated once by the ceremony, and its magic-link arm uses MAGIC_LINK_BODY, a module-private body-content pattern. Option B must not re-spell that rule. The selector is lifted into one exported definition, which the ceremony and any relaxation read, with a pin that the two cannot diverge. ⛔ No second spelling, in SQL or in a row policy.
A measured residual census. After the ceremony, count the sys_email rows the selector does not classify as identity mail but that carry no related record (related_object IS NULL AND related_id IS NULL). A non-zero count is a magic-link mail rendered from a customized template the heuristic missed, so it blocks B on that deployment. The count is taken on a real deployment's tree, not on a fixture.
Its own Clause-② widening changeset, naming what a reader gains.
What this card's cleanup does NOT depend on: it masks every copy of both mail logs and uses no selector. So it is correct before or after the ceremony, and condition 1 is independent of conditions 2 and 3.
os-dev-report addendum (patch round on PR #24722, after the seat's review record 6281834236)
B1, fixed at ee7a0710. The test double's WHERE matcher had no combinator branch. It is now a module-level matchesWhere that implements equality, $in, $gt and $and, and THROWS on every other $-combinator and on every other operator.
Re-run after a rebuild of the dogfood closure: plugin-audit typecheck exit 0; mail-log-body-migration.test.ts 13/13; audit-mail-bodies.dogfood.test.ts 4/4.
On the derivation: the miss was not in dispatch-gates. --commands did derive pnpm check:where-matcher (line 104 of the first derivation), and the first sweep ran it at exit 0. That double did not yet hold a generic matcher; it read where structurally. The silently-wrong matches came in with my later fix for check:objectql-double-limit. My re-run after that fix covered double-limit, engine-double-contract and the census, and not where-matcher. The defect is my re-run scope: after a change to a test double, I re-run every double-shaped gate (check:where-matcher, check:objectql-double-limit, check:engine-double-contract), not only the one that went red.
(c) Known limit: archived audit copies are not reached.sys_audit_log declares lifecycle.archive: { after: '90d', to: 'archive', keep: '7y' } (plugin-audit/src/objects/sys-audit-log.object.ts). On a deployment with an archive datasource, ledger rows older than 90 days are copied to that cold store and then deleted from the hot table. os migrate audit-mail-bodies reads and rewrites the hot sys_audit_log / sys_activity through the engine only. So a mail-body copy already archived keeps its body in clear in the archive datasource. Deployments with no archive datasource keep every row hot and are fully covered. This also qualifies option-B precondition 1: "the cleanup has run" covers the hot tables only; on a deployment with an archive datasource, the archived copies need their own answer. Whether that is its own card is the seat's call.
(c) Copies that sys_audit_log's 90-day lifecycle already moved to an archive datasource are not reached. That is a stated limit, judged later. It qualifies precondition 1 of "option B" (organization admins read their own mail bodies again), which also needs the ceremony merged and one exported identity selector.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: epic seat round · 2026-10-20T00:37Z
Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steve(the card's assignee)Branch:
claude/issue-24693-mirror-body-cleanupWorktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194(the epic lane)File surface, read on
origin/mainat or afterd1173825b(#24688 merged). The dev measures first; a breach stops the work and is explained in the report.os migrate audit-metadata-bodiescommand, its CLI wiring and its tests: the pattern this cleanup follows.body_text/body_htmlin existingsys_audit_logandsys_activityrows whose object issys_emailorsys_identity_email, reusing 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's mask.docs/adr/**.Container & model:
S,mode:cloud,model: TIER_DEFAULTClause-②: yes — a new operator command is a surface; the dev re-measures and corrects the PR line if it adds none
Responsibility: n/a. Residual R2 of the contract review of PR 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.
Serial constraints cleared: PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681 does not touch plugin-audit's migrate commands. If it does at the time of the merge, whichever lands second merges
main.objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24693, "status": "done", "branch": "claude/issue-24693-mirror-body-cleanup", "pr": "https://github.com/objectstack-ai/objectstack/pull/24722", "session": "session_01VHugC2zLtDQFNVzioMiUBo", "premise_still_valid": true, "summary": "Measured first: its own command, not a ceremony step. The cleanup masks every copy of both mail logs, so it needs no identity selector and is order-independent of #24681 (the ceremony re-points activity copies to sys_identity_email, which is masked the same, and leaves sys_audit_log as written). It reaches every database a pre-#24688 writer wrote to, and follows the audit-metadata-bodies precedent. Built os migrate audit-mail-bodies (cli) over plugin-audit's migrateMailLogBodyCopies: two pure planners, keyset paging, and a write of the copy column alone under preserveAudit with the stored updated_at. The report is counts only. The writer's inline mask loop became maskMailLogBodyCopy, the one helper both share; #24688's MAIL_LOG_OBJECTS / MAIL_LOG_BODY_FIELDS / SECRET_MASK are reused, not re-spelled. On the 18.0 fixture (base d1173825b): 2 audit + 2 activity copies held a clear body before, 0 after, every other byte identical, and a re-run is a no-op. Option-B precondition recorded below.", "tests": "At 1f2b0cb7/9b67ec01, then the merged head 6da0543a. plugin-audit full: 59 files, 953 passed (959 after the main merge). cli unit tier: 314 files, 4643 passed. cli integration on the touched files (absent-database, one-shot family): 2 files, 151 passed. json-stdout-purity.e2e (nightly): 64/65; the 1 red is organization-ownership's stderr case, a command this diff does not touch, NOT MEASURED on base. New pins: mail-log-body-migration.test.ts 13/13; audit-mail-bodies.dogfood.test.ts 4/4 (real binary over the 18.0 fixture; budget declared 20 s, measured 12.84 s). Ablation via ablation-replace.mjs (fix committed first): the planner's mask call made a no-op, OS_SKIP_DTS rebuild, dist preflight marker present in 2 built files; unit 7 failed/13, dogfood 2 failed/4 (dry-run count, 0-clear). Restored blob == HEAD, full rebuild, --absent preflight clean, tree clean, then 13/13 and 4/4. R1: pnpm dev --fresh on a random port, health 200, admin sign-in 200, 0 error lines, stopped. R2+R3: upgrade-18x-harness + identity-mail-read-gate + new pin, 3 files, 32 passed (pre- and post-merge). R4: serve-walled-posture-refusal 5 passed. typecheck exit 0 for plugin-audit, cli, dogfood.", "gates": "dispatch-gates --commands derived 104 (105 after the census files joined). Exit captured before any pipe: 98 exit 0. Fixed, then exit 0: check-tenant-audit-census + --self-test (the runner's one new write call site; regenerated with --write, page figures 230->231, 74->75, 129->130), check:objectql-double-limit (the fake find now applies the bound by presence). NOT MEASURED, exit 3 prerequisite: check-plugin-teardown-shape --self-test (shallow clone), spec check:skill-examples and check:dual-build-cjs-loads (need a full-workspace build). check:merge-driver exit 0; check:engine-double-contract exit 0; check-nul-bytes exit 0; --ran reconciled 105/105. check-governed-merges --branch: 0/13 paths governed, 1130 changed lines. PR check-runs at report time: 'No other open PR may claim the same issue' success; Lint/Type Check/Check Changeset/Governed Surface Queue Guard/Test Core in_progress.", "mcp_calls": "5 — issue_read x2 (card, comments), pull_request_read x3 (PR 24681 get, PR 24688 comments, PR 24722 check runs); no write tool", "api_writes": "2 — fleet-write relay: pr_create (POST repos/objectstack-ai/objectstack/pulls + assignees, read back identical); comment on issue 24693 (this report). Plus git push (not REST).", "open_questions": [], "out_of_scope_findings": [ "carrier: the upgrade-harness fixture owner (#24522 lane) · noted, not filed · the fixture dump (sqlite-text-dump.ts) carries tables and indexes only, so the sys_activity rotation VIEW is absent and any read-only command boot over the fixture reads no activity rows; the new pin restores the view and says so", "carrier: none · noted, not filed · json-stdout-purity.e2e 'os migrate organization-ownership --json still shows every boot diagnostic on stderr' red on this tree (nightly tier); command untouched by this diff; base not measured" ] }Precondition for option B (organization administrators read their own non-identity mail bodies again)
Recorded by the os-dev of this card. Option B means relaxing the
sys_emailbody field gate (requiredPermissions: ['manage_platform_settings']onbody_text/body_html) so that an organization administrator reads the bodies of their own organization's non-identity mail. It is admissible only when ALL of the following hold, on the deployment and in the code:os migrate audit-mail-bodies --apply(this card's PR) has run on the deployment and a re-run reportsrewritten: 0. Reason: the mirror read redaction serves asys_audit_log/sys_activitycopy the fields the data plane serves for its parent. Relax thesys_emailbody gate while pre-fix copies still hold bodies in clear, and those copies re-open to organization administrators, including the identity mail written intosys_emailbefore the ceremony. The audit log keepsobject_name = 'sys_email'for those rows: the ceremony does not rewrite the ledger.whereof theidentity-mail-moverow ofSIBLING_MOVESinpackages/cli/src/utils/organization-ownership-apply.ts(PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681, read on headedb2db1e8). That is raw SQL evaluated once by the ceremony, and its magic-link arm usesMAGIC_LINK_BODY, a module-private body-content pattern. Option B must not re-spell that rule. The selector is lifted into one exported definition, which the ceremony and any relaxation read, with a pin that the two cannot diverge. ⛔ No second spelling, in SQL or in a row policy.sys_emailrows the selector does not classify as identity mail but that carry no related record (related_object IS NULL AND related_id IS NULL). A non-zero count is a magic-link mail rendered from a customized template the heuristic missed, so it blocks B on that deployment. The count is taken on a real deployment's tree, not on a fixture.What this card's cleanup does NOT depend on: it masks every copy of both mail logs and uses no selector. So it is correct before or after the ceremony, and condition 1 is independent of conditions 2 and 3.
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report addendum (patch round on PR #24722, after the seat's review record 6281834236)
B1, fixed at
ee7a0710. The test double's WHERE matcher had no combinator branch. It is now a module-levelmatchesWherethat implements equality,$in,$gtand$and, and THROWS on every other$-combinator and on every other operator.pnpm check:where-matcherexit 0 (566 discovered, 0 silent, 0 unjudged).check:objectql-double-limitexit 0.check:engine-double-contractexit 0.mail-log-body-migration.test.ts13/13;audit-mail-bodies.dogfood.test.ts4/4.dispatch-gates.--commandsdid derivepnpm check:where-matcher(line 104 of the first derivation), and the first sweep ran it at exit 0. That double did not yet hold a generic matcher; it readwherestructurally. The silently-wrongmatchescame in with my later fix forcheck:objectql-double-limit. My re-run after that fix covered double-limit, engine-double-contract and the census, and not where-matcher. The defect is my re-run scope: after a change to a test double, I re-run every double-shaped gate (check:where-matcher,check:objectql-double-limit,check:engine-double-contract), not only the one that went red.Residuals (no code change)
json-stdout-purity.e2e, theos migrate organization-ownership --jsonstderr case: per the seat, a main red since feat(cli)!: 18.0 guards — os migrate organization-ownership refuses, and os serve refuses a walled posture at boot #24480 (ae6bd1a69), cured by steps 5+6 (feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681). Not this diff's.content/docs/deployment/cli.mdx,docs/qa/platform-checklist/areas/cli.jsonandpackages/cli/src/utils/schema-migrate.one-shot-family.integration.test.tsare touched by both PRs. No hunk conflicts (no ceremony file is touched here). The corrected sentence for the seat to write, since the dev writes the body once: "No ceremony file is touched. Three files are shared with PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681 (cli.mdx,cli.json,schema-migrate.one-shot-family.integration.test.ts), in hunks that do not overlap; whichever lands second mergesmain."sys_audit_logdeclareslifecycle.archive: { after: '90d', to: 'archive', keep: '7y' }(plugin-audit/src/objects/sys-audit-log.object.ts). On a deployment with anarchivedatasource, ledger rows older than 90 days are copied to that cold store and then deleted from the hot table.os migrate audit-mail-bodiesreads and rewrites the hotsys_audit_log/sys_activitythrough the engine only. So a mail-body copy already archived keeps its body in clear in the archive datasource. Deployments with noarchivedatasource keep every row hot and are fully covered. This also qualifies option-B precondition 1: "the cleanup has run" covers the hot tables only; on a deployment with an archive datasource, the archived copies need their own answer. Whether that is its own card is the seat's call.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed and closed · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-20T03:48Z. PR #24722 has merged as7e7e605de.TIER_DEFAULT, in-seat. FAIL 6281834236 at6da0543a(B1: the test double read a$-combinator as a field name), then PASS 6282296174 atee7a07103Delivered.
os migrate audit-mail-bodiesmasks the mail bodies thatsys_audit_logandsys_activitycopies 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 still hold in clear.--apply.maskMailLogBodyCopy.Carried:
json-stdout-purity'sos migrate organization-ownership --jsonstderr case has been red onmainsince feat(cli)!: 18.0 guards — os migrate organization-ownership refuses, and os serve refuses a walled posture at boot #24480. PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681 cures it.cli.mdx,cli.jsonandschema-migrate.one-shot-family.integration.test.tswith this PR. No hunk conflicts, and its next main merge takes them.sys_audit_log's 90-day lifecycle already moved to anarchivedatasource are not reached. That is a stated limit, judged later. It qualifies precondition 1 of "option B" (organization admins read their own mail bodies again), which also needs the ceremony merged and one exported identity selector.release/17.xis cut, as for 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.The card closes
completed. The dev session is archived.