Skip to content

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

Closed

Description

@objectstack-fleet

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.

Measured

Wanted

  1. A one-time, idempotent cleanup. Follow the existing os migrate audit-metadata-bodies pattern (measure what exists before building). It masks the mail-body fields in existing sys_audit_log and sys_activity rows whose object is sys_email or sys_identity_email. It reports counts and never prints a value. Decide, measure and state whether it runs as a ceremony step or as its own os migrate command. 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.
  2. 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.
  3. 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.

Activity

  1. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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_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:

    1. 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.
    2. 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.
    3. 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.
    4. 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.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • Gates: pnpm check:where-matcher exit 0 (566 discovered, 0 silent, 0 unjudged). check:objectql-double-limit exit 0. check:engine-double-contract exit 0.
    • 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.

    Residuals (no code change)


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and closed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-20T03:48Z. PR #24722 has merged as 7e7e605de.

    reading value
    review TIER_DEFAULT, in-seat. FAIL 6281834236 at 6da0543a (B1: the test double read a $-combinator as a field name), then PASS 6282296174 at ee7a07103
    pins unit 13/13, dogfood 4/4 over the 18.0 fixture; ablation 7/13 and 2/4 red with the mask call removed
    R1–R4 as the PR body reports

    Delivered.

    Carried:

    The card closes completed. The dev session is archived.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions