Compare commits

...
5 changed files with 1026 additions and 0 deletions
+10
View File
@@ -5189,3 +5189,13 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
2026-10-20 19:34:28 UTC | Gander row 196 build paused at design mismatch | Before: approved design promised Unix tester cleanup only for the owned inode. After: scratch test shows Node/libuv close unlinks a replacement inode before JS close handlers. Evidence agents/gander/work/queue-196/evidence/tester-cleanup-blocker.txt. Sent Sage/Duckworth gander-0016/0017, recommending explicit same-uid residual with private 0700 parent retained. Interpreter, concurrent peer, principal, human, peer-transport, transport, tester and process single files passed. Partial code uncommitted, tester transport not wired. No suites, mutant batches or live actions.
2026-10-21 18:45 UTC | Gander row 196 preservation before rebase | Sage-2051, Mos order mos-1193, explicitly authorizes commit and fast-forward push of the current build before deferred full verification. Product and evidence preserved together; prior focused checks and stopped gates are recorded in the row196 packet, with no claim of full acceptance. Next: rebase onto a36523713 on new gander/row196-r2, report range-diff/patch-id/conflicts, then await Sage's clear for M1-M29 and all requested suites. No live action, force push, branch deletion or tests during hold.
2026-10-21T21:11:44Z | Della mos-1213 / Vikunja 460 planning phase | Before: PRD 1.10 has no email feature; MP 95b0298 and EP 7a64921 are read-only prior inputs. Scope: independent post-v1 email plan on della/email-plan, no implementation/live access/MVP or queue changes. Gitea issue enrollment belongs to Mos/Sage because this seat has no API credential.
2026-10-21T21:12:18Z | Della mos-1213 / Vikunja 460 planning verification | After: docs/plans/2026-10-21_email-plan-della.md is 341 lines, with 13 post-v1 rows and six Jason questions. Verified 42 pinned citation anchors, ten requested coverage areas, identical prior src trees, append-only records, clean whitespace and repository queue guard. No configured CI pipeline or CI-queue wrapper found in the current foundation; documentation-only validation applies, no product suites or live tests run. Phase B/C and Gitea issue/queue enrollment remain with Mos/Sage. Only della/email-plan is authorized for push.
2026-10-21T21:37:07Z | Della mos-1220 / Vikunja 460 phase B cross-review | Before: independent plans Della 191dff32f and Ludwig 4a27b73ac delivered. After: docs/plans/2026-10-21_email-review-by-della.md reviews all 218 Ludwig plan lines, records ten findings, five grafts, six retained strengths, nine disagreements and eleven merged Jason questions. Ludwig review 143415779 read in full after ludwig-0002 arrived; reconciliation records new mail-access action, no agent reroute and correction of Della dataRoot credential example. Original plan unchanged. Planning only, no product/MVP/queue/live change.
2026-10-21T21:43:02Z | Della mos-1223 / Vikunja 460 phase C synthesis | Before: independent plans/reviews complete, Mos selected Della base. After: docs/plans/2026-10-21_email-plan.md merges Ludwig grafts and accepted corrections, with 16 post-v1 rows (eight READY, eight WAIT/optional), eleven deduplicated Jason questions, per-row test paths and explicit Q210 non-v1 scheduling. Original Della plan/review preserved; Ludwig originals remain on their branch. Static checks cover graft/row/question/citation anchors and original blobs. No source code, suite, mailbox, secret file, service, MVP or queue change. Candidate goes to Ludwig before Sage landing/queue handoff, copying Mos.
2026-10-21T21:45:20Z | Della mos-1223 / ludwig-0004 focused corrections | Before: b096a16cc left content-key location implicit and E05 core registration marked ready. After: merged plan 399 lines fixes external configDir secret key/lifecycle namespace outside dataRoot backup/restore, E01 schema and E03 missing/expired-key restore tests, and E05/E06 test-local registry readiness with core kinds.mjs registration gated on approved E01/Q4. Sixteen rows and eleven questions retained; originals unchanged. Focused static checks and queue guard apply, no product/live changes. Reviewer will check changed lines only.
+4
View File
@@ -1432,3 +1432,7 @@ are never rewritten or removed; corrections are new entries.
2026-10-21T19:46:12Z | Dewey (Opus 5.5, 84fb346c) | row 222 r1 static review (megavolt-0095), c9aa9c2ee, manifest c2121b55 | APPROVE, comment 30138 on #1723, queue rev 2966; rulings: createObject=rename accepted, 5 s human timeout deferred with STATUS codes in the follow-up, credential.helper reset argv-only accepted; notes N1-N5; no tests run
2026-10-21T19:49:37Z | Sage (Opus 5.5, 1ef1e4f8) | row 250 landing, row 196 r5 grant (mos-1198) and resume (mos-1200), F27 row 253 | 250 LANDED 1b6158ce2 (patch-id 29786caa, Filbert rev 2957, Gizmoduck slot green), #1755 closed c30139, Vikunja 450; 196 r5 91e3696ef hunk checked by Duckworth, webui 304/0; my queue add tripped Gander's busy check, not a stop (mos-1200), resume on 05e940117; row 253 #1758 brief 0cdead66c
2026-10-21T20:46:03Z | Sage (Opus 5.5, 1ef1e4f8) | row 222 landing, PRD 1.9 and 1.10, Pocket ID design row 255, row 254 brief, 196 r6 grants | 222 LANDED 0b33f5e9d (megavolt-0098 gate, Dewey rev 2966, patch-id edb60b4f), #1723 closed c30147, done rev 2972; follow-up row 254 (#1759, after 196); PRD 1.9 f48725cfc (REQ-WEB-2, REQ-DEC-3 amended, mos-1202); 458 brief 7d08237e1 (#1760), row 255 rev 2976; PRD 1.10 add0f1aae REQ-INST-2 post-v1 (mos-1204, tasks 97/0, bus req 3/0); Vikunja 458 c478, 416 c479; queue-commit 21def2850 revs 2968-2976; 196 r6 gate gander-0066 2334/0, 38/38 killed; 244 granted sage-2147; 196 r6 resume granted sage-2145 after self-tee busy refusal (mos-1205). Miss: landed 222 before checking 196's manifest; 196 carried results voided. Class rule now (mos-1204, replaces the mos-1200 interface grep): before any landing push, compare every in-review and in-progress row's manifest against the landing's paths.
2026-10-21T21:12:18Z | Della (GPT-6.1-Sol, mos-1213) | Vikunja 460 independent post-v1 email plan, della/email-plan from c3d88840c | 341-line proposal, 13 post-v1 rows, six Jason questions, 42 pinned citation anchors verified. Canonical queue next della: nothing. Planning only, no live mail/service/MVP/queue change; phase B/C with Mos, no Ludwig plan read. Commit and branch push authorized by dispatch.
2026-10-21T21:37:07Z | Della (GPT-6.1-Sol, mos-1220) | Vikunja 460 phase B email cross-review, della/email-plan | Ludwig plan 4a27b73ac and review 143415779 read in full. Cross-review has ten findings and eleven merged questions; original Della plan unchanged. Canonical queue next della: nothing. Static verification only; review push followed by direct della-0002 handoff to both threads under Q210. Phase C base choice remains with Mos.
2026-10-21T21:43:02Z | Della (GPT-6.1-Sol, mos-1223) | Vikunja 460 phase C merged email plan, della/email-plan | Mos-selected base plus Ludwig grafts; original plans/reviews unchanged. Sixteen post-v1 rows, eight READY and eight WAIT/optional; eleven Jason questions. Canonical queue next della: nothing. Docs-only checks. Commit/push then immediate della-0003 handoff to Ludwig/Mos; Sage landing/queueing follows approval.
2026-10-21T21:45:20Z | Della (GPT-6.1-Sol, mos-1223 / ludwig-0004) | merged email plan focused review fixes | External content-key/expiry location and restore tests specified; fake registry work READY, production registry gated on PRD approval/Q4. Merged plan 399 lines, original plans/reviews unchanged. Commit/push and focused Ludwig review before Sage handoff.
+341
View File
@@ -0,0 +1,341 @@
# Post-v1 email processing and monitoring plan (Della)
**Status:** proposal for Mos phase A, Vikunja 460, mos-1213. Planning only.
**Goal:** receive mail for named agents and users through Stack-owned connections,
then expose authorized reading, routing and drafting equally to every harness.
**Boundary:** every build row below is **post-v1**. None is required for the
2026-10-27 MVP. This document changes neither PRD 1.10 nor the live Stack.
No service, mailbox connection, live-mail read, account, DNS change or spend is authorized.
R21 binds the specialist: **“The email specialist reads and drafts, never sends.”**
## Evidence and reuse
Citations are pinned, not claims about deployed behavior. Prefixes below identify
the repository and commit for every `file:line` citation. Both prior clones were
inspected read-only. No prior application or test suite was executed.
| Prefix | Pinned evidence |
| --- | --- |
| S | Stack `c3d88840c5b6ffa62d2a6a4aa4385d388234d777`, this worktree |
| MP | `mail-processor` `95b02989f4d4276dded00c90ffd74ebb7d03ae8b` |
| EP | `email-processor` `7a6492190cd376058e29a04fd0593e86bae1a8e5` |
| SM | `origin/huey/system-manager-design` at `2ee68cd69337bb933a8fabeb04bcd71835170171`, Q198 r6, superseding r5 `fd44f7c60` |
**F1 Existing mail code:** recursive comparison found identical `src/` trees in MP
and EP. Reuse once, not as two running products. Both implement normalized messages
and a connector boundary (`src/email_processor/connectors/base.py:8,30`), M365
pagination/rate-limit handling (`connectors/m365.py:81,138`, under that same source
directory), ordered AND/OR rules (`src/email_processor/rule_engine.py:81,124`), and
bounded preview/cold-start safety (`src/email_processor/poller.py:101,122,152`).
Port these concepts and selected synthetic tests into Stack-native modules after
dependency/license review. Neither copy proves IMAP or POP3 support: connector
selection handles only M365 (`src/email_processor/poller.py:48`).
**F2 Prior intent:** EP `docs/PRD.md:25,39` describes a single-organization M365
application, not the Stack add-on. MP `docs/PRD.md:26,66,204,235` specifies generic
Graph/IMAP/POP3 drivers, separated intake/execution, fail-closed activation and
atomic staging/checkpoints. Those are useful design requirements, not implemented
features. Adopt the safety properties and conformance cases, not its independent
tenant/profile product, bootstrap authority or mandatory standalone web setup.
**F3 Drop unsafe coupling:** in both repos, `src/email_processor/poller.py:186,200,301`
checks for duplicates before effects and advances a timestamp to wall-clock now.
`src/email_processor/models.py:266` has indexes, not a unique mailbox/message key.
Replace these with durable provider cursors, unique identities and transactional
delivery. Drop connector caches keyed just by mailbox name (`poller.py:48`), direct
GLPI/ntfy effects (`action_executor.py:30,69,159`) and source category writes
(`connectors/m365.py:170`, `poller.py:359`). Drop direct model calls and content logs
(`classifiers/llm.py:79,105,109,123`), replacing decisions through Q207.
Do not import deployment manifests, credential settings, existing accounts or data.
**F4 Stack enforcement:** S `packages/harness/src/tools.mjs:1` already maps typed
tools through the runner socket with broker checks. Its MCP bridge carries no
capability (`packages/harness/src/mcp-server.mjs:1,34`). Reuse that path for all
harnesses, rather than giving each one mailbox credentials or a monitoring plugin.
S `packages/bus/src/broker.mjs:690,701,1601,1666` supplies human-approved grant
consumption and action authority. `packages/bus/src/credentials.mjs:142,358,379`
supplies scoped references and protected use. Mail kinds and grants do **not** exist:
S `packages/business/src/vocabulary.mjs:109,119` accepts Gitea, Vikunja and host kinds.
Mail requires reviewed extensions, not relabelling mail credentials as an existing kind.
**F5 Product constraints:** S `docs/prd/mosaic-stack.md:432,454,477,496` defines
chief-raised grants, exact human approval, one-time consumption and immediate local
revocation. Its `:507,542,582,607` covers decision evidence, thread binding and hard
waits. PRD “inbox” is the decision inbox, not email. Mail needs a separately approved
post-v1 PRD addendum. S `packages/plugins/src/kinds.mjs:9,32` has no mail-source kind.
Follow REQ-PLUG-1/2 (S PRD `:876,910`) for provider adapters.
**F6 Monitoring precedent:** S `tools/agent-watch/jarvis-email-watch-liveness.sh:8,26,53`
checks timer state, failed execution and staleness. Reuse the distinction between
alive, failed and unknown, not its legacy unit names or fleet wiring. SM
`docs/plans/2026-10-21_q198-system-manager.md:262,291,304,326` supplies protected mode,
independent probes and controlled resume. It is a design dependency, not shipped proof.
## Architecture and sources
**D1 One owner per instance.** Add an optional supervised `packages/mail/` service,
with a single leased owner per configured source. It owns connection pools, OAuth
refresh, scheduling, cursors and intake. A harness can stop, restart or change without
losing monitoring. Multiple accounts are isolated by instance/business/source ID.
“One service” does not mean one global credential, mailbox or shared authorization.
Default installation is disabled and opens no connection.
Use Node modules aligned with the Stack, adapting the Python design rather than
embedding both applications and their separate databases/authentication/UI. A Python
sidecar would reuse more syntax but duplicate operational boundaries. Harness-native
connectors would repeat connection state and fail Jason's cross-harness requirement.
**D2 Source contract.** Propose a reviewed `mail-source` plugin kind, list-selected,
with `descriptor()`, `scan({source,cursor,limits,signal})`,
`fetch({source,item,part,limits,signal})` and `health({source,signal})` exports.
`scan` returns bounded observations, continuation, terminal cursor and identity epoch.
Adapters receive protected credential-use callbacks, never agent-provided secret paths.
Core owns grants, leases, persistence, rule evaluation and audit. No adapter sends,
deletes, labels, moves or marks messages read. Use the existing channel kind only
where its dispatch contract fits presentation, not as a substitute polling interface.
| Source | Proposed implementation and boundary |
| --- | --- |
| Microsoft 365 | Graph read-only access, per-folder delta/continuations and immutable message IDs. Replace timestamp polling. Restrict application rights to named mailboxes and verify effective scope, including any additive Entra permissions. |
| IMAP | TLS with certificate verification, OAuth where supported, otherwise a separately approved service/app credential. Read-only selection and `BODY.PEEK`; UIDVALIDITY plus UID, never sequence number. IDLE is a wake-up hint with periodic reconciliation. |
| Other providers | Prefer a conforming IMAP source initially. Gmail API and JMAP are later adapters only when a named need justifies them. No new provider account is implied. |
| POP3 | Deferred optional row. Require stable UIDL, TLS and read-only retrieval. No `DELE`. Reject automatic activation without safe identity/reconciliation. |
Graph delta is folder scoped and cursors are opaque ([Microsoft delta documentation](https://learn.microsoft.com/en-us/graph/delta-query-messages)).
Immutable IDs survive moves within a mailbox, not archive-mailbox moves or reimports
([Microsoft ID documentation](https://learn.microsoft.com/en-us/graph/outlook-immutable-id)).
Mailbox-scoped Application RBAC and unscoped Entra grants are independent
([Microsoft RBAC documentation](https://learn.microsoft.com/en-us/exchange/permissions-exo/application-rbac)).
IMAP identity, read-only selection and PEEK semantics follow [RFC 9051](https://www.rfc-editor.org/rfc/rfc9051.html).
These are protocol references checked 2026-10-21, not evidence of installed support.
**D3 Addressing.** Version a route table mapping trusted delivery evidence to a stable
Stack user or role-instance ID, never a display name or a newly invented role.
Aliases on an approved ingress mailbox are the recommended inexpensive start.
Plus-addresses are acceptable only after fixtures and a later authorized provider test
prove the exact recipient survives delivery. They are not a security boundary.
Dedicated mailboxes provide stronger provider-side separation for confidential users
or agents, at the cost of account management and possible licenses.
Bind routes to source identity plus provider-controlled original-recipient evidence.
`To`/`Cc`, forwarded headers and arbitrary `Delivered-To` headers are insufficient
unless the enrolled delivery path establishes their provenance. Bcc or ambiguous
alias delivery goes to restricted operator review, never guessed routing. Unknown
addresses do not create agents. Multiple recipients produce independently authorized
deliveries without revealing other recipients' mail. Jason approves domains, names,
aliases, mailboxes and each provider-side change before provisioning. No SMTP/MX
server, catch-all domain or public webhook is proposed for the initial increment.
## Processing, data and retention
**D4 Intake pipeline:** approved source → bounded discovery → bounded parse → durable
observation/dedup → versioned deterministic rules → authorized delivery outbox →
Stack notification/work request. Provider cursor advancement and durable observations
commit together under the source lease/generation. Outbox delivery has a stable key
and a recipient-side unique receipt, so a crash after delivery but before acknowledgment
does not create a second task/message. Never promise exactly-once external effects.
Default candidates: poll every 60 seconds, at most 50 observations and 10 MiB fetched
per slice, fair scheduling between sources. Persist continuation when the slice fills.
Cap each source at 1,000 pending deliveries and 100 MiB of retained content initially.
Persist quota accounting across restarts and slices. Reaching a quota holds new intake
and raises an alert, while read-only health checks continue.
First activation establishes a from-now baseline without actions on old mail. Historical
import is a separate approved time/count window, previewed before activation. Cap
violations, disk-full and unavailable audit storage hold work without advancing past
unstaged mail. These limits are proposed settings to validate, not measured capacities.
Provider identity is scoped to business/source/account: Graph immutable ID, IMAP
folder/UIDVALIDITY/UID or POP3 UIDL. RFC Message-ID, thread ID and content similarity
are secondary correlation signals, never sole proof of uniqueness. Preserve distinct
messages with missing or reused Message-ID. An IMAP move, epoch reset, expired delta
cursor, archive move or reimport enters bounded reconciliation. Record aliases only
with sufficient identity evidence. Otherwise hold possible duplicate actions for review.
An edit is a new observation version, not automatic permission to rerun prior actions.
Preserve dedup tombstones beyond content expiry. Replay needs a new authorized intent,
parent link and current grants. A rule edit never retroactively executes preview work.
**D5 Parsing and rules.** Preserve provenance for normalized sender, recipients,
timestamp, subject, text, thread references and attachment descriptors. Bound MIME
depth to 10, parts to 100 and normalized text to 256 KiB. Decode safely, flag malformed
or truncated content and quarantine failures. Prefer text, sanitize HTML for display,
disable remote images/URLs and never execute active content. Rules use finite allowlisted
fields/operators, stable priority and first-match by default. Reject executable rules
and unbounded regex. Record rule/version and routing reason. Unmatched mail is held
for its authorized owner, not dropped or copied fleet-wide. Routing a user notification
does not launch an agent. Agent wake-up is a separate admission-controlled work request.
Optional classification and any mail-triggered decision go through the Stack's one
decision module under DECISIONS Q207. The mail service neither selects nor falls back
to a model locally. Core policy still determines permitted actions and human hard waits.
Use minimal, explicitly permitted content and an allowlisted result schema. A model
result is advisory data, never a grant. Missing model service leaves deterministic
intake working and model-dependent work held. Summaries/drafts run as ordinary governed
agent work, not an extra mail-service model client.
**D6 Attachments.** Initially enumerate metadata only. An authorized explicit fetch
quarantines bytes under an opaque ID, with a 10 MiB per-part and per-message total cap.
Never trust filenames as paths, unpack archives, execute macros or follow embedded
links. Content extraction requires isolated no-network processing, resource bounds,
scanner success and a separate read right. Failed, encrypted or unsupported files stay
quarantined. A clean scan does not make attachment instructions trustworthy.
**D7 Storage.** Proposed `<dataRoot>/mail/<business>/` contains encrypted content blobs,
SQLite observation/outbox state, encrypted cursors, local drafts and append-only event evidence.
No mail content enters git or images. Service-only directories are 0700, files 0600.
Use scoped keyed fingerprints for private dedup fields. Separate content keys from
provider slots. Keep tokens, delta URLs and authentication errors out of ordinary logs.
Default durable records hold opaque IDs, state, versions and counts, not bodies,
subjects, recipient lists or attachment names. Content access is explicit and audited.
Recommended content policy for an enabled pilot: encrypted bodies and local drafts
for 30 days, explicitly fetched attachments for 7 days, minimal metadata/tombstones for
365 days. Without Jason's approved content/retention policy, persist metadata only and
show that later content fetch requires the source online. Expired content returns an
explicit unavailable result. Define backup/key expiry and restore suppression together:
restoring an old backup must not resurrect expired content or redeliver old work.
Local pruning needs the Stack's `data.delete` approval path or an explicitly approved
future retention authority. Do not silently grant automatic deletion. Write receipts,
preserve legal holds and do not rewrite `<dataRoot>/runs/` or existing audit evidence.
## Access and security
**D8 Identical access contract.** Proposed tools are `mail_list(source?,cursor?,limit)`,
`mail_get(id,view)`, `mail_attachment_get(id,part)`, `mail_route(id,target,expect)`,
`mail_ack(id,expect)`, `mail_draft_create(id,text)` and `mail_health(source?)`.
All map to typed broker verbs via the existing tool socket/MCP bridge. Local CLI and
authenticated Console use the same handlers. List is paginated (default 20, max 100).
`ack` changes Stack-local state only. Drafts are local unsent records, not Graph drafts.
Health returns only authorized source metadata. No arbitrary URL, shell, source secret
path, provider query language or send tool is accepted.
Separate rights for metadata, body, attachment, draft and routing per business,
source, address binding and named principal. Recheck current role/grant on every read,
notification and action, including caches and derived summaries. Revoke immediately
fences new uses, invalidates fetch handles and cancels owned in-flight reads before
returning content. Agents receive only mail explicitly routed to their grant scope.
User mail requires that user's or explicitly delegated access. An email specialist is
not implicitly allowed to read every user's mail. Cross-business IDs return no data.
New mail access grants follow the existing chief → `credential.mint` → authenticated
human Approve chain, with exact target, rights and expiry bound once. Add the mail
grant method and action routing only through a Jason-approved PRD version and reviewed
vocabulary/role changes. Until then new actions remain hard waits. Within-role mail
access can become routine only inside those approved grants. Manual-mode actions still
need per-use approval. A hand-edited grant or approving email creates no authority.
**D9 Credentials and hostile input.** Distinguish a source service principal's
provider connection grant from an agent's mediated mail-access grant. Recommend
dedicated read-only ingress credentials per source/business, held by protected Stack
code and never shared with agents. This system-service ownership is a new approved
extension, not an exception silently taken to REQ-CRED-1. Configuration names only
an approved absolute slot path, for example
`<dataRoot>/credentials/mail/<business>/<source>/oauth`; no value or partial value is
read by a planner, returned through tools or embedded in a plan. Missing/expired/revoked
slots fail closed. Validate owned non-symlink files, rotation and revocation behavior.
Treat mail, headers, filenames, OCR and quoted replies as untrusted external data.
Sender addresses, even authenticated messages, cannot approve decisions, change policy,
mint grants or impersonate the CLI/Console human. Delimit model input and strip tool
authority, but rely on broker denial, sandboxing and least privilege for enforcement.
An email asking an agent to fetch secrets, forward documents or run commands must not
gain those rights. External action proposals use existing approval paths and provenance.
No automatic link fetches. Provider endpoints are enrolled allowlists with TLS and
redirect checks, not values selected from mail content. Logs use reason codes and IDs.
**D10 Sending.** Out of scope: SMTP submission, Graph send, forwarding, auto-replies,
DSNs generated by this service and emailed digests. Block them in adapter interfaces,
credentials and network policy. R21 stays intact even if a future separate sender role
is approved. Human export of an unsent draft is not authorization for Stack delivery.
## Monitoring and system-manager integration
**D11 Health:** report desired/actual connection state, last successful scan and durable
cursor commit, lag, backlog, lease owner, retry-after, credential status, quarantine,
outbox age and disk pressure. Never report “healthy” merely because the process lives.
Candidate stale threshold: three missed scan intervals. Alert once per incident, on
severity change and recovery through existing internal notifications/Console, not mail.
Show alert delivery failure locally. Alerts include IDs and reason codes, no content.
Register dependency scopes `mail-source/<business>/<source>` with Q198's manager.
Network/provider outages pause only affected intake and dependent fetch/dispatch.
Independent sources and authorized stored-mail reads can continue. Disk/audit failure
halts new intake/effects. Auth/config denial stays held for correction, never retries
under another credential. Malformed messages quarantine individually, not as outages.
Apply Q205–Q207 through protected mode, not the human kill switch: mechanical source
probes run independently at 60, 120, 180 seconds, increasing by 60 to a 600-second
ceiling, indefinitely, while respecting longer Retry-After. No JEV/model availability
gate and no model call for a mail health check. Mail health proves TLS/auth and scoped
source access without fetching message bodies. One lease owns retries, so adapter and
manager loops cannot multiply them. Disabled sources make no probes.
On recovery reconcile cursors/uncertain delivery receipts, pass one synthetic pipeline
canary, then resume eligible work in manager-controlled batches without approval per
session. Never send test mail or use a live message as a synthetic canary. Uncertain
external effects stay fenced. Human kill remains human-only to clear. These mail
extensions to SM D13–D15 need contract review and tests before operational reliance.
## Ordered build rows (all post-v1, not current queue edits)
Each row is a separately reviewable deliverable. Write failing synthetic cases first,
implement only that row, then run its named tests and affected existing suites. Commands
below name proposed tests. They are not tests run during this planning assignment.
Shared touch points include `packages/{bus,business,harness,plugins,cli,webui}/`.
No rollout uses an implementation candidate until normal review and re-pin gates pass.
| Row | Deliverable / files | Acceptance evidence |
| --- | --- | --- |
| E01 **post-v1** | PRD addendum and source/data schemas, `docs/prd/`, `packages/mail/schemas/`. Resolve Q1–Q6 before activating corresponding behavior. | Schema fixtures reject unknown fields, missing scopes and secret values. Disabled add-on makes zero connections and v1 startup works without it. Obtain Jason's policy approval, not implementation's implied consent. |
| E02 **post-v1** | Protected source slots and mail-access grants, `packages/bus/`, `packages/business/`. Build D8/D9 on existing grant executor. Depends E01. | `node --test packages/bus/tests/mail-grants.test.mjs`: deny non-chief raises, nonhuman approvals, altered targets, duplicate consumption, hand grants and revoked/in-flight access. Secret sentinel absent from all outputs. Existing grant/approval suites pass. |
| E03 **post-v1** | Durable intake ledger, leases, outbox and disabled supervisor, `packages/mail/src/{store,intake,service}.mjs`. Depends E02. | `node --test packages/mail/tests/intake.test.mjs`: crash at each transaction/outbox boundary, concurrent owners, disk-full, cap exhaustion and restart. No skipped cursor or duplicate recipient effect, no source calls while disabled. |
| E04 **post-v1** | Bounded MIME parsing and quarantine, `packages/mail/src/{parse,attachments}.mjs`. Depends E03. | `node --test packages/mail/tests/parse.test.mjs`: malformed MIME, oversized/deep parts, traversal filenames, remote HTML, archives and encrypted attachments. Limits enforced before expansion, zero execution/network, explicit truncation/quarantine. |
| E05 **post-v1** | M365 driver and first `mail-source` kind, `packages/plugins/src/kinds.mjs`, `packages/plugins/mail-source/m365/`. Depends E03/E04. | `node --test packages/mail/tests/m365.test.mjs`: fake Graph pagination/delta, immutable ID moves, cursor expiry, token expiry, 429, restarts and scoped-denial cases. Read-only request allowlist, no skipped/repeated delivery. Existing plugin tests pass. |
| E06 **post-v1** | IMAP driver, `packages/plugins/mail-source/imap/`. Depends E05 contract. | `node --test packages/mail/tests/imap.test.mjs`: fake TLS/IMAP server, certificate denial, IDLE disconnect, sequence renumbering, UIDVALIDITY reset, folder moves and missed wake-ups. No Seen/STORE/EXPUNGE, uncertain identities hold effects. |
| E07 **post-v1** | Address table, bounded rules and delivery, `packages/mail/src/{addresses,rules,route}.mjs`. Depends E03–E06. | `node --test packages/mail/tests/routing.test.mjs`: aliases, plus tags, Bcc, forged recipient headers, retired agents, multiple recipients, missing/reused Message-ID and duplicate provider pages. No cross-principal disclosure, no duplicate work, no authority from sender. |
| E08 **post-v1** | Broker verbs, MCP/typed tools and local drafts, `packages/bus/`, `packages/harness/`, `packages/mail/src/access.mjs`. Depends E02/E07. | `node --test packages/harness/tests/mail-tools.test.mjs`: identical schema/results/refusals through every supported harness adapter, read/attachment/draft rights separated, manual mode and expiry enforced, send impossible. Existing MCP/tools/approval suites pass. |
| E09 **post-v1** | Decision-module integration, `packages/mail/src/decisions.mjs`, central decision interface. Depends E08 and approved Q198/Q207 interface. | `node --test packages/mail/tests/decisions.test.mjs`: model outage leaves rules active, one central fallback path, schema-invalid suggestions held, no mail-local model client, hostile mail cannot approve/grant/spend/send. Minimal-context privacy and budget checks pass. |
| E10 **post-v1** | Health, alerts and protected-mode recovery, `packages/mail/src/health.mjs`, Q198 manager adapter. Depends E03/E08 and implemented/verified SM D13–D15. | `node --test packages/mail/tests/outage.test.mjs`: fake clock/provider verifies outage scope, full cooldown sequence, Retry-After, parallel probes, no model probe, no outage DLQ, restart continuity, canary/batched resume, uncertain delivery fencing and human kill persistence. |
| E11 **post-v1** | Authorized Console/CLI mail views, retention/export/prune/restore, `packages/{mail,cli,webui}/`. Depends E08/E10 and Q4 authority. | Focused `mail-retention` and `mail-ui` tests prove ACLs on lists/search/export, expired-content denial, exact delete approval, holds, backup-expiry restore suppression and no old-work replay. Decision inbox remains distinct, alerts survive mail outage. |
| E12 **post-v1** | Opt-in operator runbook and acceptance packet under `docs/`. Depends E01–E11. | Offline end-to-end synthetic message → parse → route → authorized tools → local draft, under every supported harness. Demonstrate revoke, outage recovery and disable/rollback preserving evidence. Live pilot is a separate Jason-authorized step after named address/account/scope/retention approvals. No external mail sent. |
| E13 **post-v1 optional** | POP3 adapter, `packages/plugins/mail-source/pop3/`, only for an approved named need. Depends E12. | Fake server tests UIDL absence/reuse, restart and TLS failure, retrieval caps, reconciliation and zero DELE. Unsupported identity refuses activation. Other adapters remain unaffected. |
## Questions for Jason (recommendations, not assumed approvals)
1. **Q1 Source and address pilot:** which provider, domain and existing or new mailbox
should receive agent/user mail? **Recommend** one separately approved read-only
ingress mailbox, aliases for named agent/user recipients, no catch-all or MX change.
Use dedicated mailboxes for confidential separation. Prove preserved delivery
identity before enabling alias/plus routing. New addresses/accounts wait for Jason.
2. **Q2 Grant ownership:** approve a protected source service principal plus separately
scoped per-agent/user mail-access grants? **Recommend yes**, through a post-v1 PRD
amendment and existing chief/human approval model. No agent gets provider secrets
or implicit access to personal mail. Enable only the explicitly requested sources.
3. **Q3 Automation:** may rules route mail and wake a granted agent without per-message
human approval? **Recommend yes** for reviewed within-role routes, subject to manager
admission and manual mode. Reading/drafting does not authorize requested external
effects. Any model-needed decision uses the one Q207 module.
4. **Q4 Content and deletion:** approve the 30-day body/draft, 7-day attachment and
365-day minimal-evidence policy, including encrypted backups and deletion authority?
**Recommend these pilot limits**, metadata-only until approved, with an exact
`data.delete` approval binding for pruning. Do not weaken the hard-waits list.
Jason must identify any business retention/hold requirements before live intake.
5. **Q5 Exposure and costs:** allow provider webhooks or paid mailbox infrastructure?
**Recommend no initially**. Use outbound Graph polling/IMAP, existing internal
notifications and no new exposure. Each future webhook, DNS/MX change or spend
gets its own explicit authorization.
6. **Q6 Sending:** should any future role send email? **Recommend keep sending out
of this add-on**. Retain R21 permanently for the specialist. Any later sender is a
separate design with exact-recipient approval, scoped credentials and effect evidence,
not a toggle that silently turns drafts into delivery.
## Planning receipt and handoff
Verified pinned commits, source-tree equality, cited code boundaries and ten requested
coverage areas. No implementation, dependency installation, legacy test execution,
mail connection or live-stack change occurred. Canonical `queue next della` returned
`nothing` after the linked-worktree refusal. No queue/MVP row changed. Vikunja 460 is
the supplied tracking task. Mos/Sage owns any required Gitea issue and future queue
enrollment because this seat has no Gitea API credential. Phase B review and phase C
synthesis remain with Mos. Della has not read Ludwig's plan. This branch push delivers
the planning artifact only, not approval to build or deploy it.
+399
View File
@@ -0,0 +1,399 @@
# Post-v1 email processing and monitoring plan
**Status:** phase C candidate, mos-1223, Vikunja 460. Ludwig reviews before Sage lands it.
**Goal:** Stack-owned mail intake, monitoring, authorized reading/routing and local drafting for every harness.
**Boundary:** all rows post-v1, none required for 2026-10-27. No PRD 1.10 or live change here.
Q210 ready rows go to non-v1 seats after handoff. No ready label authorizes live connections,
addresses/accounts, DNS/MX, exposure, spend or external mail. Original plans/reviews stay unchanged.
R21: **“The email specialist reads and drafts, never sends.”**
## Evidence and reuse
Citations are pinned, not claims about deployed behavior. Prefixes below identify the repository and commit
for every `file:line` citation. Both prior clones were inspected read-only. No prior application or test suite
was executed.
| Prefix | Pinned evidence |
| --- | --- |
| S | Stack `c3d88840c5b6ffa62d2a6a4aa4385d388234d777`, this worktree |
| MP | `mail-processor` `95b02989f4d4276dded00c90ffd74ebb7d03ae8b` |
| EP | `email-processor` `7a6492190cd376058e29a04fd0593e86bae1a8e5` |
| SM | `origin/huey/system-manager-design` at `2ee68cd69337bb933a8fabeb04bcd71835170171`, Q198 r6, superseding r5 `fd44f7c60` |
**F1 Existing mail code:** recursive comparison found identical `src/` trees in MP and EP, including identical
tests. MP contains EP plus three commits. Reuse once. Both implement normalized messages and a connector
boundary (`src/email_processor/connectors/base.py:8,30`), M365 pagination/rate-limit handling
(`connectors/m365.py:81,138`, under that same source directory), ordered AND/OR rules
(`src/email_processor/rule_engine.py:81,124`), and bounded preview/cold-start safety
(`src/email_processor/poller.py:101,122,152`). Port all 36 rule-engine and 15 predeployment safety test cases
as synthetic JS cases after dependency/license review. Remove subject logging (`rule_engine.py:147`). Neither
copy proves IMAP or POP3 support: connector selection handles only M365 (`src/email_processor/poller.py:48`).
**F2 Prior intent:** EP `docs/PRD.md:25,39` describes a single-organization M365 application, not the Stack
add-on. MP `docs/PRD.md:26,66,204,235` specifies generic Graph/IMAP/POP3 drivers, separated intake/execution,
fail-closed activation and atomic staging/checkpoints. Those are useful design requirements, not implemented
features. Deployment manifests target USC, not proof of a currently running service. Do not enroll an employer
tenant by default or change any existing service. Adopt the safety properties and conformance cases, not its
independent tenant/profile product, bootstrap authority or mandatory standalone web setup.
**F3 Drop unsafe coupling:** in both repos, `src/email_processor/poller.py:187-195,200,301` checks for
duplicates before effects and advances a timestamp to wall-clock now. `src/email_processor/models.py:266` has
indexes, not a unique mailbox/message key. Replace these with durable provider cursors, unique identities and
transactional delivery. Drop connector caches keyed just by mailbox name (`poller.py:48`), direct GLPI/ntfy
effects (`action_executor.py:30,69,159`) and source category writes (`connectors/m365.py:170`,
`poller.py:359`). Drop direct model calls and content logs (`classifiers/llm.py:79,105,109,123`), replacing
decisions through Q207. Do not import deployment manifests, credential settings, existing accounts or data.
Graph already selects `body` and normalizes HTML (`connectors/m365.py:27,129`), although `body_text` is empty.
**F4 Stack enforcement:** S `packages/harness/src/tools.mjs:1` already maps typed tools through the runner
socket with broker checks. Its MCP bridge carries no capability (`packages/harness/src/mcp-server.mjs:1,34`).
Reuse that path for all harnesses, rather than giving each one mailbox credentials or a monitoring plugin. S
`packages/bus/src/broker.mjs:690,701,1601,1666` supplies human-approved grant consumption and action
authority. `packages/bus/src/credentials.mjs:142,358,379` supplies scoped references and protected use. Mail
kinds and grants do **not** exist: S `packages/business/src/vocabulary.mjs:109,119` accepts Gitea, Vikunja and
host kinds. Mail requires reviewed extensions, not relabelling mail credentials as an existing kind.
**F5 Product constraints:** S `docs/prd/mosaic-stack.md:432,454,477,496` defines chief-raised grants, exact
human approval, one-time consumption and immediate local revocation. Its `:507,542,582,607` covers decision
evidence, thread binding and hard waits. PRD “inbox” is the decision inbox, not email. Mail needs a separately
approved post-v1 PRD addendum. S `packages/plugins/src/kinds.mjs:9,32` has no mail-source kind. Follow
REQ-PLUG-1/2 (S PRD `:876,910`) for provider adapters.
**F6 Monitoring precedent:** S `tools/agent-watch/jarvis-email-watch-liveness.sh:8,26,53` checks timer state,
failed execution and staleness. Reuse the distinction between alive, failed and unknown, not its legacy unit
names or fleet wiring. SM `docs/plans/2026-10-21_q198-system-manager.md:262,291,304,326` supplies protected
mode, independent probes and controlled resume. It is a design dependency, not shipped proof. S
`packages/discord/src/connector.mjs:1-12` and `journal.mjs:60,71` provide authorize → journal → engine →
deliver and intent/result reconciliation precedents. Reuse principles, not Discord nonce assumptions or its
old approval-button path for mail authorization.
**F7 Secret-scan evidence:** Ludwig reports a pattern-based scan of both prior trees and full histories with
no candidate credential value found (his plan `4a27b73ac`, `docs/plans/2026-10-21_email-plan-ludwig.md:35`).
Della did not independently repeat that scan. It proves neither absence of secrets nor that rotation is
unnecessary. Any later finding is reported by path and commit only, never by value or partial value.
## Architecture and sources
**D1 One owner per instance.** Add an optional supervised `packages/mail/` service, with a single leased owner
per configured source. It owns connection pools, OAuth refresh, scheduling, cursors and intake. A harness can
stop, restart or change without losing monitoring. Multiple accounts are isolated by instance/business/source
ID. “One service” does not mean one global credential, mailbox or shared authorization. Default installation
is disabled and opens no connection.
Use Node modules aligned with the Stack, adapting the Python design rather than embedding both applications
and their separate databases/authentication/UI. A Python sidecar would reuse more syntax but duplicate
operational boundaries. Harness-native connectors would repeat connection state and fail Jason's cross-harness
requirement.
**D2 Source contract.** Propose a reviewed `mail-source` plugin kind, list-selected, with `descriptor()`,
`scan({source,cursor,limits,signal})`, `fetch({source,item,part,limits,signal})` and `health({source,signal})`
exports. `scan` returns bounded observations, continuation, terminal cursor and identity epoch. Adapters
receive protected credential-use callbacks, never agent-provided secret paths. Core owns grants, leases,
persistence, rule evaluation and audit. No adapter sends, deletes, labels, moves or marks messages read. Use
the existing channel kind only where its dispatch contract fits presentation, not as a substitute polling
interface.
| Source | Proposed implementation and boundary |
| --- | --- |
| Microsoft 365 | Graph `Mail.Read`, read-only access, per-folder delta/continuations and immutable message IDs. Replace timestamp polling. Restrict application rights to named mailboxes and verify effective scope, including any additive Entra permissions. |
| IMAP | TLS with certificate verification, OAuth where supported, otherwise a separately approved service/app credential. Read-only selection and `BODY.PEEK`; UIDVALIDITY plus UID, never sequence number. IDLE is a wake-up hint with periodic reconciliation. |
| Other providers | Prefer a conforming IMAP source initially. Gmail API and JMAP are later adapters only when a named need justifies them. No new provider account is implied. |
| POP3 | No build row until Jason names a POP-only need. A future adapter requires stable UIDL, TLS, read-only retrieval and safe identity/reconciliation. No `DELE`. |
Graph delta is folder scoped and cursors are opaque ([Microsoft delta
documentation](https://learn.microsoft.com/en-us/graph/delta-query-messages)). Immutable IDs survive moves
within a mailbox, not archive-mailbox moves or reimports ([Microsoft ID
documentation](https://learn.microsoft.com/en-us/graph/outlook-immutable-id)). Mailbox-scoped Application RBAC
and unscoped Entra grants are independent ([Microsoft RBAC
documentation](https://learn.microsoft.com/en-us/exchange/permissions-exo/application-rbac)). IMAP identity,
read-only selection and PEEK semantics follow [RFC 9051](https://www.rfc-editor.org/rfc/rfc9051.html). These
are protocol references checked 2026-10-21, not evidence of installed support.
**D3 Addressing.** Version a route table mapping trusted delivery evidence to a stable Stack user or
role-instance ID, never a display name or a newly invented role. The org file holds this human-approved table
through its versioned audited write path. No agent reroutes messages or widens its own route. A route grants
no read right. The first source follows Q1’s provider. Offline adapters need not wait for that choice. Review
maintained exact-pinned IMAP/MIME dependencies in their owning rows, initially Ludwig’s
`imapflow`/`mailparser` candidates. Do not write either protocol/parser in-house. Aliases on an approved
ingress mailbox are the recommended inexpensive start. Plus-addresses are acceptable only after fixtures and a
later authorized provider test prove the exact recipient survives delivery. They are not a security boundary.
Dedicated mailboxes provide stronger provider-side separation for confidential users or agents, at the cost of
account management and possible licenses.
Bind routes to source identity plus provider-controlled original-recipient evidence. `To`/`Cc`, forwarded
headers and arbitrary `Delivered-To` headers are insufficient unless the enrolled delivery path establishes
their provenance. Bcc or ambiguous alias delivery goes to restricted operator review, never guessed routing.
Unknown addresses do not create agents. Multiple recipients produce independently authorized deliveries
without revealing other recipients' mail. Jason approves domains, names, aliases, mailboxes and each
provider-side change before provisioning. No SMTP/MX server, catch-all domain or public webhook is proposed
for the initial increment.
## Processing, data and retention
**D4 Intake pipeline:** approved source → bounded discovery → bounded parse → durable observation/dedup →
versioned deterministic rules → authorized delivery outbox → Stack notification/work request. Provider cursor
advancement and durable observations commit together under the source lease/generation. Outbox delivery has a
stable key and a recipient-side unique receipt, so a crash after delivery but before acknowledgment does not
create a second task/message. Never promise exactly-once external effects.
Default candidates: poll every 60 seconds, at most 50 observations and 10 MiB fetched per slice, fair
scheduling between sources. Persist continuation when the slice fills. Cap each source at 1,000 pending
deliveries and 100 MiB of retained content initially. Persist quota accounting across restarts and slices.
Reaching a quota holds new intake and raises an alert, while read-only health checks continue. First
activation establishes a from-now baseline without actions on old mail. Historical import is a separate
approved time/count window, previewed before activation. Cap violations, disk-full and unavailable audit
storage hold work without advancing past unstaged mail. These limits are proposed settings to validate, not
measured capacities.
Provider identity is scoped to business/source/account: Graph immutable ID, IMAP folder/UIDVALIDITY/UID or
POP3 UIDL. RFC Message-ID, thread ID and content similarity are secondary correlation signals, never sole
proof of uniqueness. Preserve distinct messages with missing or reused Message-ID. An IMAP move, epoch reset,
expired delta cursor, archive move or reimport enters bounded reconciliation. Record aliases only with
sufficient identity evidence. Otherwise hold possible duplicate actions for review. An edit is a new
observation version, not automatic permission to rerun prior actions. Preserve dedup tombstones beyond content
expiry. Replay needs a new authorized intent, parent link and current grants. A rule edit never retroactively
executes preview work.
**D5 Parsing and rules.** Preserve provenance for normalized sender, recipients, timestamp, subject, text,
thread references and attachment descriptors. Bound MIME depth to 10, parts to 100 and normalized text to 256
KiB. Decode safely, flag malformed or truncated content and quarantine failures. Prefer text, sanitize HTML
for display, disable remote images/URLs and never execute active content. Rules use finite allowlisted
fields/operators, stable priority and first-match by default. Reject executable rules and unbounded regex.
Record rule/version and routing reason. Unmatched mail is held for its authorized owner, not dropped or copied
fleet-wide. Routing a user notification does not launch an agent. Agent wake-up is a separate Q198-admitted
work request. Wake requires an approved route, current access grant, sender allowlist and trusted aligned
DMARC authentication. Strip/ignore sender-supplied Authentication-Results. Ambiguous forwarded/list mail or
failed authentication goes to restricted review.
Optional classification and any mail-triggered decision go through the Stack's one decision module under
DECISIONS Q207. The mail service neither selects nor falls back to a model locally. Core policy still
determines permitted actions and human hard waits. Use a reviewed mail decision schema inside SM D12’s 4 KiB
request limit (`:503-512`). Initial input is metadata only: opaque eligible route IDs, work/capability/error
enums and bounded numeric state. Sender domain/auth fields or any new mail-derived field need explicit schema
approval and redaction tests; no free-form rule summaries or body. If metadata cannot decide, hold/unknown
rather than invent confidence. Body-based summaries and drafts are separate governed work, not mail-local
routing decisions. A model result is advisory data, never a grant. Missing model service leaves deterministic
intake working and model-dependent work held. Summaries/drafts run as ordinary governed agent work, not an
extra mail-service model client.
**D6 Attachments.** Initially enumerate metadata only. An authorized explicit fetch quarantines bytes under an
opaque ID, with a 10 MiB per-part and per-message total cap. Raw quarantine fetch needs no scanner. It never
implies permission to expose extracted content. Read-only files are not execution prevention.
Scanner/extraction is gated E16. Never trust filenames as paths, unpack archives, execute macros or follow
embedded links. Content extraction requires isolated no-network processing, resource bounds, scanner success
and a separate read right. Failed, encrypted or unsupported files stay quarantined. A clean scan does not make
attachment instructions trustworthy.
**D7 Storage.** Proposed `<dataRoot>/mail/<business>/` contains encrypted content blobs, SQLite
observation/outbox state, encrypted cursors, local drafts and append-only event evidence. No mail content
enters git or images. Service-only directories are 0700, files 0600. Use scoped keyed fingerprints for private
dedup fields. Content keys live at `<configDir>/secrets/mosaic-stack/mail-content/<business>/<key-id>`,
outside dataRoot and its backup set, separate from provider slots, with 0600/no-symlink checks.
Protected key expiry/retirement metadata also stays outside the mail-backup restore domain. Keep tokens, delta URLs and authentication errors out
of ordinary logs. Default durable records hold opaque IDs, state, versions and counts, not bodies, subjects,
recipient lists or attachment names. Content access is explicit and audited.
Recommended content policy for an enabled pilot: encrypted bodies and local drafts for 30 days, explicitly
fetched attachments for 7 days, minimal metadata/tombstones for 365 days. Without Jason's approved
content/retention policy, persist metadata only and show that later content fetch requires the source online.
Expired content returns an explicit unavailable result. Define backup/key expiry and restore suppression
together: restoring an old backup must not resurrect expired content or redeliver old work. Use tested
backup-copy deletion or crypto-erasure with expiry of every relevant key copy/escrow, plus restore
suppression. Encryption or one key deletion alone proves neither. Local pruning needs the Stack's
`data.delete` approval path or an explicitly approved future retention authority. Do not silently grant
automatic deletion. Write receipts, preserve legal holds and do not rewrite `<dataRoot>/runs/` or existing
audit evidence.
## Access and security
**D8 Identical access contract.** Proposed tools are `mail_list(source?,cursor?,limit)`, `mail_get(id,view)`,
`mail_thread(id,cursor?,limit?)`, `mail_attachment_get(id,part)`, `mail_ack(id,expect)`,
`mail_draft_create(id,text)` and `mail_health(source?)`. Thread views recheck every message and omit
unauthorized members/recipients. No reroute tool. All map to typed broker verbs via the existing tool
socket/MCP bridge. Local CLI and authenticated Console use the same handlers. List is paginated (default 20,
max 100). `ack` changes Stack-local state only. Drafts are local unsent records, not Graph drafts. Health
returns only authorized source metadata. No arbitrary URL, shell, source secret path, provider query language
or send tool is accepted.
Routing is human/system-only. Separate metadata, body, attachment and draft rights per business/source and
named principal. Recheck current role/grant on every read, notification and action, including caches and
derived summaries. Revoke immediately fences new uses, invalidates fetch handles and cancels owned in-flight
reads before returning content. Agents receive only mail explicitly routed to their grant scope. User mail
requires that user's or explicitly delegated access. An email specialist is not implicitly allowed to read
every user's mail. Cross-business IDs return no data.
New mediated access uses proposed `mail.access`, distinct from provider credential creation through
`credential.mint`. The chief raises it to the authenticated human Approve path, with exact
principal/target/rights/expiry bound once. Add its access grant schema and action routing only through a
Jason-approved PRD version and reviewed vocabulary/role changes. Until then new actions remain hard waits.
Grant creation stays human-only. Read/draft actions become routine only inside approved grants. Manual mode needs
per-use approval. A hand-edited grant or approving email creates no authority.
**D9 Credentials and hostile input.** Distinguish a source service principal's provider connection grant from
an agent's mediated mail-access grant. Recommend dedicated read-only ingress credentials per source/business,
held by protected Stack code and never shared with agents. This system-service ownership is a new approved
extension, not an exception silently taken to REQ-CRED-1. Configuration names only an approved absolute slot
under `<configDir>/secrets/mosaic-stack/mail/<business>/<source>/`. Proposed service-principal schema: `{kind:
"mail-service", business, source, provider, mailbox, scopes, slotPath, rotateBy, approvalId, revision}`.
Identity comes from approved configuration, not an agent argument. It grants source reads only, not human
input, grant issuance or general bus writes. Reject wildcard/unknown fields, expired approval, changed source
identity and slots inside repo/dataRoot. This extends existing schemas explicitly (S
`packages/business/src/credentials.mjs:95-102`, `packages/webui/src/sso.mjs:33`). No value or partial value is
read by a planner, returned through tools or embedded in a plan. Missing/expired/revoked slots fail closed.
Validate owned non-symlink files, rotation and revocation behavior.
Treat mail, headers, filenames, OCR and quoted replies as untrusted external data. Sender addresses, even
authenticated messages, cannot approve decisions, change policy, mint grants or impersonate the CLI/Console
human. Delimit model input and strip tool authority, but rely on broker denial, sandboxing and least privilege
for enforcement. An email asking an agent to fetch secrets, forward documents or run commands must not gain
those rights. External action proposals use existing approval paths and provenance. No automatic link fetches.
Provider endpoints are enrolled allowlists with TLS and redirect checks, not values selected from mail
content. Logs use reason codes and IDs. Mail tool results carry `untrusted: true` plus a notice modeled on S
`packages/bus/src/broker.mjs:109`. Sanitize hidden HTML, comments and control-character tricks for display
without treating sanitization as authorization. Test body, subject, display-name, filename, hidden-text and
spoofed-internal-sender injection. Mail-woken work uses a fresh isolated profile, intersected with its
existing grants: mail reads/draft, scoped fleet messages and decision.raise only, no git/credential/
external-message tools. Broker enforcement matches the tool list. Decisions and onward messages retain
`source: mail`; downstream seats cannot launder mail into authority. A powerful existing thread is never
resumed as the restricted mail turn.
**D9a Service delivery:** add a protected business-scoped delivery principal/API that accepts only approved
recipients and stable mail receipt keys, with fixed actor `mail`. S `packages/bus/src/principal.mjs:8` has no
mail principal today. Do not impersonate a user/agent or call an unauthenticated general bus write. Journal
delivery acceptance and recipient receipts atomically in the bus through this API, with grant/revoke
checks. The separate local outbox retries the same receipt key after a crash, not a distributed transaction.
**D10 Sending.** Out of scope: SMTP submission, Graph send, forwarding, auto-replies, DSNs generated by this
service and emailed digests. Block them in adapter interfaces, credentials and network policy. R21 stays
intact even if a future separate sender role is approved. Human export of an unsent draft is not authorization
for Stack delivery.
## Monitoring and system-manager integration
**D11 Health:** report desired/actual connection state, last successful scan and durable cursor commit, lag,
backlog, lease owner, retry-after, credential status, quarantine, outbox age and disk pressure. Never report
“healthy” merely because the process lives. Candidate stale threshold: three missed scan intervals. Alert once
per incident, on severity change and recovery through existing internal notifications/Console, not mail. Show
alert delivery failure locally. Alerts include IDs and reason codes, no content.
Register dependency scopes `mail-source/<business>/<source>` with Q198's manager. Network/provider outages
pause only affected intake and dependent fetch/dispatch. Independent sources and authorized stored-mail reads
can continue. Disk/audit failure halts new intake/effects. Auth/config denial stays held for correction, never
retries under another credential. Permit at most one refresh/retry of a rejected authentication attempt, then
stop and alert to prevent account lockout. Missing grants/config and explicit scope denials get no retry.
Malformed messages quarantine individually.
Apply Q205–Q207 through protected mode, not the human kill switch: mechanical source probes run independently
at 60, 120, 180 seconds, increasing by 60 to a 600-second ceiling, indefinitely, while respecting longer
Retry-After. No JEV/model availability gate and no model call for a mail health check. Mail health proves
TLS/auth and scoped source/folder access without bodies: IMAP login/NOOP alone and Graph directory `GET
/users/{id}` do not prove it. The latter needs directory permissions, not Mail.Read ([Microsoft user
API](https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0)). Use minimal scoped mailbox
operations and test denied folders/mailboxes. Retention or provider deletion can lose source mail during
outages; detect/report gaps, never promise losslessness. One lease owns retries, so adapter and manager loops
cannot multiply them. Disabled sources make no probes.
On recovery reconcile cursors/uncertain delivery receipts, pass one synthetic pipeline canary, then resume
eligible work without approval per session in manager-controlled batches at
most five with 30-second healthy intervals, and current capacity/admission checks (SM `:326-339`). After three
consecutive failed automatic exits, persist `recovery-escalated`; probes continue, work does not. Only the
authorized `system.recovery.release` path with incident/revision and diagnosis clears that latch, never the
kill switch (SM `:341-358`). Never send test mail or use a live message as a synthetic canary. Uncertain
external effects stay fenced. Human kill remains human-only to clear. These mail extensions to SM D13–D15 need
contract review and tests before operational reliance.
## Ordered build rows (all post-v1)
READY means no Jason decision blocks the stated offline deliverable. It does not mean its code prerequisites
have landed. Sage queues it now and assigns an available non-v1 seat when prerequisites/review gates pass,
without a scheduled start or calendar wait. WAIT Qn identifies the positive authority needed before that row's
governed integration. Unanswered questions never block independent READY work. All rows remain disabled by
default, use synthetic mail/fake providers and run named tests plus affected existing suites. No ready row
enables a real source or weakens current grants/hard waits. Q3/Q9/Q10 propose keeping restrictive defaults,
not gates on code that preserves them. Q7 is a technical dependency review, not a new permission gate. Q11
affects neither build nor pilot because no old repository/service is altered. Queue enrollment/landings belong
to Sage after Ludwig approves this plan, with the v1 path and protected files preserved.
| Row / gate | Deliverable and dependencies | Acceptance tests (proposed filenames, not run here) |
| --- | --- | --- |
| E01 **post-v1 READY**; Jason: none for draft | PRD addendum draft, source/data/access/service and external content-key location/lifecycle schemas in `docs/prd/`, `packages/mail/schemas/`. Core mail-source registration/policy activation awaits approved addendum/Q4; content policy awaits E13/Q6. | `packages/mail/tests/schema.test.mjs`: unknown fields, wildcards, secret values, incomplete scopes and mismatched principal/source rejected; key location excludes dataRoot/backups, mode/symlink checks, disabled schema compatible with v1. Every requirement maps to a row. |
| E02 **post-v1 WAIT Q4** | Protected slot reader, source-service enrollment/credential lifecycle and separate `mail.access` grant action in `packages/{bus,business}/`. After E01 and approved PRD. No real credential enrollment in this build row. | `packages/bus/tests/mail-grants.test.mjs`, `packages/bus/tests/mail-principal.test.mjs`: non-chief/nonhuman approval, modified targets, expiry, duplicate consume, hand grants, failed issuance undo, manual mode, immediate revoke/in-flight fencing and secret sentinel leakage. Reject repo/dataRoot slots; existing grant tests pass. |
| E03 **post-v1 READY**; Jason: none | Durable scoped ledger, encrypted state, leases, cursor/continuation, outbox and quota accounting in `packages/mail/src/{store,intake,service}.mjs`. After E01. No dependency on live grants. | `packages/mail/tests/intake.test.mjs`, `packages/mail/tests/store.test.mjs`: crash every staging/delivery boundary, concurrent owners, missing/reused Message-ID, source-scoped identity collisions, 1,000/100-MiB quotas, disk/audit failure, restart and zero-connection disabled startup. Missing/expired key fails closed; restored backup with expired key returns unavailable, never plaintext. No skips or duplicate recipient effects. |
| E04 **post-v1 READY**; Jason: none, Q7 advisory | Bounded MIME parsing/metadata quarantine in `packages/mail/src/{parse,attachments}.mjs`. After E01; independently of grants. Review license/advisories and exact-pin maintained MIME dependency. | `packages/mail/tests/parse.test.mjs`, `packages/mail/tests/parser-corpus.test.mjs`: multipart, bad charset, missing IDs, S/MIME, 30-MiB input, depth/part/text limits, traversal, hidden HTML, comments, bidi/zero-width display tricks, archive and encrypted attachment quarantine. Fuzz corpus; no expansion/execution/network, no content logs. |
| E05 **post-v1 READY** offline; core registration **WAIT Q4** | Graph descriptor/module in `packages/plugins/mail-source/m365/` against test-local registry `packages/mail/tests/fixtures/mail-source-registry.mjs`. After E01/E03/E04. `packages/plugins/src/kinds.mjs` stays unchanged until E01 addendum approval/Q4; core entry lands together with or after that approved addendum, with its first production module. Q1 selects pilot, not offline work. | `packages/mail/tests/m365.test.mjs`, `packages/mail/tests/source-contract.test.mjs`: fake Graph delta/paging, immutable moves, token/cursor expiry, 401 single refresh, 429/5xx scheduling, restart, denied mailbox, additive-scope check and alias/plus/Bcc provenance. No source writes; test-local registry works with unchanged core registry before approval. Existing plugin suite passes. |
| E06 **post-v1 READY**; Jason: none, Q7 advisory | IMAP module `packages/plugins/mail-source/imap/`. After E05's test-local shared contract; no core registration before approved E01/Q4. Review/license/exact-pin maintained IMAP dependency. | `packages/mail/tests/imap.test.mjs`: local fake TLS server, certificate denial, selected-folder scope denial, BODY.PEEK/no Seen or STORE/EXPUNGE, IDLE drop/poll fallback, renumbering, UIDVALIDITY/move reconciliation and alias/plus/Bcc/forged-header provenance. Ambiguity holds effects. |
| E07 **post-v1 READY**; Jason: none for pure evaluation | Address/rule evaluator `packages/mail/src/{addresses,rules,route}.mjs`. After E01/E03/E04; no org-file activation or delivery privilege. | `packages/mail/tests/rules.test.mjs`, `packages/mail/tests/routing.test.mjs`: port all 36 prior rule cases; bounded operators, rule version/order, previews never execute, history import window, recipient ambiguity, multiple recipients, retired agents, repeated identical mail and no content logs. |
| E08 **post-v1 WAIT Q4/Q5** | Approved org route writer and narrow service delivery API/principal in `packages/{bus,business}/`, `packages/mail/src/delivery.mjs`. After E02/E03/E07. No agent route writer/reroute. | `packages/bus/tests/mail-delivery.test.mjs`, `packages/bus/tests/mail-routes.test.mjs`: exact human approval and versioned changes, fixed actor/business, recipient receipt idempotency, spoofed principal refused, route addition grants no read, revoke before dispatch fences content, no human-input impersonation. |
| E09 **post-v1 WAIT Q4** | Mediated typed/MCP tools, per-message thread view and local drafts in `packages/{bus,harness}/`, `packages/mail/src/access.mjs`. After E02/E08. | `packages/harness/tests/mail-tools.test.mjs`, `packages/mail/tests/access.test.mjs`: same schema/results/refusals on all supported harnesses, separate metadata/body/attachment/draft rights, page caps, thread filtering, cross-business ID denial, revoke/cache races, manual mode, local ack/draft and absence of send/reroute tools. |
| E10 **post-v1 WAIT Q5** | Restricted mail-triggered launch profile, sender-auth gate and origin propagation in `packages/{mail,harness,bus}/`. After E08/E09 and Q198 admission interface. | `packages/mail/tests/mail-turn.test.mjs`, `packages/mail/tests/injection.test.mjs`: fresh context/grant intersection, no git/credential/external tools at broker and harness, body/subject/display-name/filename injection, forged auth headers, aligned DMARC/allowlist, forwarded ambiguity, no resume of powerful thread, no authority laundering through fleet messages/decisions. |
| E11 **post-v1 WAIT Q4** (mail decision schema approval) | `packages/mail/src/decisions.mjs` using the one approved Q207 decision API; no local model client. After E07/E09 and implemented Q198 decision module. | `packages/mail/tests/decisions.test.mjs`: 4-KiB closed schema, no body/free text, extra metadata denied until approved, out-of-enum/missing/uncalibrated confidence held, central fallback only, unavailable model leaves deterministic intake working; useful metadata-only outcomes or explicit unknown. |
| E12 **post-v1 READY**; Jason: none for synthetic recovery | Health/manager adapter `packages/mail/src/health.mjs`. After E03/E08 and implemented/verified SM D13-D15; fixtures can precede integration. | `packages/mail/tests/health.test.mjs`, `packages/mail/tests/outage.test.mjs`: mailbox/folder scope proof, full 60..600 cooldown and Retry-After, independent probes/one retry owner, bad-auth single retry then stop, no model probes or outage DLQ, synthetic canary, five-item/30-second batches, three-exit latch/release, human kill, alert-once/recovery and source gap reporting. |
| E13 **post-v1 WAIT Q4/Q6** | Authorized CLI/Console mail/draft/digest views and content/export/retention handlers in `packages/{mail,cli,webui}/`. After E09/E12. No live purge; keep `data.delete` hard wait. | `packages/mail/tests/retention.test.mjs`, `packages/cli/tests/mail.test.mjs`, `packages/webui/tests/mail.test.mjs`: per-user ACL on every item/export, separate decision inbox, exact deletion approval/holds/receipts, expiry of backup content and all relevant keys, restore suppression/tombstones, local draft ownership and metadata-only digest. |
| E14 **post-v1 READY**; Jason: none for offline proof | Operator runbook/acceptance packet in `docs/`; offline end-to-end harness test. After E01-E13. | `packages/mail/tests/predeploy-safety.test.mjs`, `packages/mail/tests/end-to-end.test.mjs`: port all 15 safety-gate cases; synthetic intake → route → authorized tools → local draft across supported adapters, zero source connections while disabled, from-now baseline, bounded preview, revoke/outage/restart and disable/rollback with evidence preserved. |
| E15 **post-v1 WAIT Q1/Q4/Q5/Q6**, plus Q2 if personal mail | Explicit separately authorized live pilot: enrolled agent ingress source, approved grant/route/content policy and reviewed re-pin. After E14. No v1 gate or external test mail. | `packages/mail/tests/pilot-reconcile.test.mjs` validates a redacted identity-level acceptance manifest; authorized operator supplies live receipts for delivered/held/missing identities, recipient proof, health, restart/revoke and bounded catch-up. Counts alone prove neither loss nor duplication. Complete on coverage/evidence, not a mandatory week or scheduled wait. |
| E16 **post-v1 OPTIONAL WAIT Q8**, plus Q4 access | Isolated scanner/content extractor only if Jason chooses attachment content support, `packages/mail/src/extract.mjs`. After E04/E09, separate reviewed dependency/resource boundary. | `packages/mail/tests/extract.test.mjs`: no-network/resource isolation, scan failure/unavailable/encrypted/unsupported hold, execution/archive expansion prohibited, hostile extracted text marked untrusted, access/revoke checked before release. No scanner implies no extracted-content feature. |
Run proposed Node files with `node --test <filename>`; owning reviewers select affected existing suites and
require green evidence before commit/landing. Pure store/parser/rule rows can proceed independently of
approval work. Later adapters Gmail API/JMAP/POP3 require a named need and separate rows, not a committed
expansion of this add-on.
## Questions for Jason (merged list, recommendations are not approvals)
1. **Q1 Pilot provider/domain/addresses:** which controlled domain, provider and agent ingress mailbox,
aliases or plus-tags? Recommend one approved non-USC agent mailbox, aliases after recipient proof, no
catch-all/MX change. Dedicated mailbox only for approved isolation. Its protocol pilots first; build both
adapters offline meanwhile.
2. **Q2 Personal mail scope:** which existing user mailbox, if any, may be read? Recommend none initially.
Later enrollment names user/mailbox and exact consent or delegation, separately from agent ingress.
3. **Q3 Source mutation:** mark read, flag, label, move or save provider drafts? Recommend no, read-only
sources and Stack-local drafts/acknowledgments.
4. **Q4 Service/access grants:** approve protected source-service credential ownership, distinct
per-agent/user `mail.access` approval, mail-source kind and closed mail decision schema? Recommend yes through reviewed
post-v1 PRD/vocabulary changes, chief/human approval, exact targets/rights/expiry/revoke. New access action
stays a hard wait until the approved PRD explicitly routes it. Provider credential creation remains
distinct.
5. **Q5 Unattended wake:** which senders/routes may wake granted agents without per-message approval?
Recommend allowlist plus trusted aligned DMARC, approved route, restricted fresh profile and Q198
admission. Manual mode overrides this. Ambiguous/failing mail quarantines. Neither identity nor email
approves external effects.
6. **Q6 Content/retention/deletion:** what may be retained, for how long, with what holds? Recommend encrypted
30-day bodies/drafts, 7-day explicitly fetched attachments, 365-day minimal evidence/tombstones,
metadata-only until approved. Keep exact `data.delete` authorization, backup/key expiry and restore
suppression.
7. **Q7 Dependencies:** maintained IMAP/MIME packages after license/security review and exact pins rather than
custom clients/parsers? Recommend yes, investigate Ludwig's imapflow/mailparser candidates in owning rows.
Routine technical review unless an existing repository policy adds an approval gate; no calendar wait for
package review.
8. **Q8 Attachments:** allow bytes/extraction and require scanning? Recommend metadata first, explicit capped
raw fetch into quarantine, no scanner for opaque quarantine. Extracted content waits for isolated
no-network processing, resource limits and a scanner. Do not enable arbitrary execution or automatic
archive expansion.
9. **Q9 Exposure/spend:** new listener/webhook, DNS/MX, paid infrastructure or model spend? Recommend none
initially. Outbound monitoring/internal alerts suffice. Every later exposure, address/account change or
spend needs its own authorization.
10. **Q10 Sending:** should this add-on send/forward/reply? Recommend no. R21 remains permanent for the
specialist. A future separate sender needs its own approved plan and exact-recipient human approval
through `message.external`.
11. **Q11 Prior repositories/service:** is the configured USC deployment still running, and should either
repo/service be archived or retired? Recommend no operational change here. Preserve both histories, use MP
as consolidated design reference. Any archival/retirement is a separate authorized task after actual-state
verification.
## Verification and handoff
Sources: Della 191dff32f, Ludwig 4a27b73ac, reviews 9182134e5/143415779, Mos's routing ruling.
Verify citations, coverage, row gates/tests and unchanged original blobs. No product suite,
live mail, secret-file read, install or service change here. After Ludwig approval, send Sage
this pin for refactor landing and READY-row queueing on non-v1 seats, copying Mos.
Resolve review changes without a calendar wait. Jason-gated work waits on its numbered decision.
@@ -0,0 +1,272 @@
# della-0002: phase B review of Ludwig's email plan
2026-10-21. Mos assignment mos-1220, Vikunja 460. Planning review only.
**Recommendation:** neither proposal is ready for implementation approval. Preserve
Ludwig's ingress/turn-isolation ideas and Della's grant/durability/retention controls.
Mos chooses the base in phase C. Neither original plan was edited for this review.
## Evidence pins
Every `file:line` below resolves at its named commit. Abbreviations:
| Ref | Repository/commit and document |
| --- | --- |
| L | Stack `4a27b73ac`, `docs/plans/2026-10-21_email-plan-ludwig.md`, read all 218 lines |
| D | Stack `191dff32f`, `docs/plans/2026-10-21_email-plan-della.md`, 341 lines |
| S | Stack `c3d88840c`, the implementation and PRD 1.10 base for both plans |
| MP | `mail-processor` `95b0298` |
| EP | `email-processor` `7a64921` |
| SM | Stack `2ee68cd69`, `docs/plans/2026-10-21_q198-system-manager.md` r6 |
MP contains EP plus three commits. `src/` and `tests/` compare identical. There are
36 rule test definitions and 15 predeployment safety test definitions. This confirms
L `:17,28,33`, not that any tests pass today. No prior app or suite was executed.
## 1. Wrong or unsupported claims and material gaps
**F1 (factual): Graph already fetches body content.** L `:25` says it never fetches
a body. MP and EP `src/email_processor/connectors/m365.py:27` explicitly select
`body`; `:118,129` stores HTML body content and `:135` retains the provider object.
`body_text=""` is a normalization gap, not absence of body retrieval. Correct before
using that claim to estimate migration scope or privacy exposure.
**F2 (high): role plus route is not the complete grant model.** L `:139,145,147`
makes those the access conditions, but its rows `:185-195` omit mail grant creation,
exact human approval, expiry, failed-issuance undo, revoke races and manual mode.
S `docs/prd/mosaic-stack.md:432,454,477,496,565,607` requires those controls and
keeps new actions hard waits until an approved PRD says otherwise. S
`packages/bus/src/broker.mjs:690,701` consumes approved grants once. Add explicit
source-service enrollment and per-principal mail-access grant work and tests.
Routing changes must never grant body access merely by adding a seat to a route.
Use a new approved mail-access action for mediated rights, not `credential.mint`
unless an operation actually creates a provider credential. Both use authenticated
human decision records, with new action routing governed by REQ-WAIT-3.
**F3 (high): dedup conflates content, identity and authorization.** L `:121` gives
Graph an apparently unscoped immutable-ID unique key and makes `(source_id,sha256)`
unique. L `:123` merges across sources by Message-ID plus body hash. Distinct deliveries
can have identical content or reused IDs, and two businesses must not share a mail
record/visibility decision. MP `docs/PRD.md:300,306,310,313` explicitly requires scoped
identity, keyed fingerprints and move/epoch reconciliation. Retain source-scoped
observations and recipient-specific delivery receipts. Correlation may link records
only inside an authorized scope, never collapse them or widen access by itself.
Test forged/reused Message-ID, identical repeated mail and cross-business collisions.
**F4 (high): durable mail storage does not prove durable delivery.** L `:118-127`
advances the cursor after a row commit, then routes and delivers. It does not define a
durable work state/outbox, recipient idempotency receipt, source lease or atomic
ownership claim. Crash after bus acceptance and before local acknowledgment can
duplicate work. A unique mail row alone cannot prevent that. MP `docs/PRD.md:334,343,349`
separates staging, concurrency and effects. S `packages/bus/schema.sql:70,83` has unique
bus message IDs and delivery history but no automatic mail-source idempotency binding.
Add transactional pending work, leased intake and stable per-recipient effect keys.
**F5 (high): mail health probes do not establish mailbox health.** L `:173` proposes
Graph `GET /users/{id}`, which checks a directory user, not access to the mailbox.
Its application permission is `User.Read.All`, not the proposed `Mail.Read`
([Microsoft Get user documentation](https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0)).
Use an approved minimal mailbox-scoped read with no bodies. Likewise IMAP login/NOOP
does not prove access to the selected folder. Both checks need enrolled source identity,
scope and capability validation, with no new privilege merely for probing.
L `:173` also resumes after one successful probe without stating the synthetic canary,
effect reconciliation, batch limits and observation intervals required by SM `:326-339`.
Its mention of three failed exits at L `:176` needs those exit stages to exist. Add the
persisted escalation latch/release behavior from SM `:341-358`. “Mail is not lost ...
because the server keeps it” is unsupported: retention, deletion, cursor expiry and
provider failure can remove mail. Report detected gaps, never promise losslessness.
**F6 (high): retention receipts are not deletion authority.** L `:131,186` proposes
`mail prune` with receipts/holds but does not bind it to the human `data.delete` approval
path. S `docs/prd/mosaic-stack.md:582-605` makes deletion a hard wait. Retain receipts
and holds, add exact deletion authorization, and test backup restore, content expiry,
dedup tombstone survival and non-redelivery. Choosing a retention period does not
silently route deletion within-role.
**F7 (medium): Q198 metadata allowlist is mis-cited and not automatically reusable.**
L `:66` points to SM `:420` for the 4 KiB restriction, but D12 is SM `:503-512`.
L `:126` adds sender domain, authentication results, subject length and rule summary.
Those fields are not explicitly in D12's closed allowlist of work/resource/queue/error/
capability/opaque-route fields. Require an approved, tested mail decision schema.
Metadata-only is a useful default, not proof of compliance or semantic classification
quality. Show deterministic or unknown routing when permitted metadata is insufficient.
Do not send a free-form rule summary that can contain user content.
**F8 (medium): the mechanical mail principal is new authority work.** L `:92,127`
assigns bus actor `mail`, but S `packages/bus/src/principal.mjs:8-11` verifies only
human/tester/user transport identities, and `packages/bus/src/broker.mjs:2965-2985`
checks session action authority for message sends. The design needs a narrowly scoped
trusted service delivery API/principal, not a spoofed agent session or human input.
Specify business identity, fixed actor provenance, recipient checks and allowed verbs.
This is a missing implementation contract, not evidence that service delivery is impossible.
**F9 (medium): recipient evidence needs a Graph gate too.** L `:108` correctly rejects
`To` as envelope proof, then suggests Graph recipient fields plus plus-tag without
showing they preserve the original alias/Bcc recipient. MP and EP
`src/email_processor/connectors/m365.py:115-126` only normalize visible To/Cc fields.
L M4 tests IMAP, while M5 `:189` has no recipient-provenance case. Test alias/plus/Bcc
preservation for every enrolled source and reject sender-supplied delivery headers.
When evidence is absent, quarantine rather than infer from `To` or a guessed tag.
**F10 (factual/qualification): narrow the evidence claims.** L `:19,54,209,218` treats
deployment configuration as current USC deployment evidence. It establishes intended
targeting, not that anything is currently running. No live check is authorized here.
L `:35` qualifies its secret scan, then concludes “Nothing to rotate.” A pattern scan
cannot establish that conclusion. Say no candidate found by the reported scan and
make no credential-validity/rotation claim. I have not repeated a full-history scan.
L `:68` says no mail code anywhere, but S
`tools/agent-watch/jarvis-email-watch-liveness.sh:2,18` is mail-watcher liveness code.
Say no integrated Stack mail intake/access service instead. L `:28,43` calls the rule
engine pure: MP `src/email_processor/rule_engine.py:147` logs the subject. Port rule
semantics, remove content logging, and do not describe the existing function as pure.
## 2. Ludwig strengths to graft into either base
- **G1 Restricted mail-triggered work:** L `:155` proposes mail-turn authority,
`source: mail` decision provenance and no git/credential/external-message tools.
D has least privilege but no explicit restricted mail-triggered launch profile.
Adopt a fresh isolated profile whose rights are the intersection with existing
grants. Do not resume a powerful ongoing thread or let fleet messaging launder
mail-origin requests into authority elsewhere. Broker enforcement must match tools.
- **G2 Trusted sender evidence before wake:** L `:127,158,213` adds provider-origin
Authentication-Results, allowlists and aligned authentication. D lacks an explicit
sender/authentication gate. Adopt it for unattended wake. A DMARC pass authenticates
a domain, not a human decision or permission. Define forwarded/list-mail handling as
quarantine/manual review initially. Strip forged auth headers at the trust boundary.
- **G3 Read-only thread tool:** L `:141` explicitly offers `mail.thread`. D has no
equivalent. Add a paginated view restricted per message, not an entire provider thread
that might contain recipients or messages the caller cannot see.
- **G4 Dependency investigation and parser corpus:** L `:88,186,202,215` names the
IMAP/MIME dependency work and concrete hostile fixtures. Carry that explicit package
review, exact pins and case migration into the row that first needs each dependency.
This review does not endorse an unverified package version or install anything.
- **G5 Explicit future live acceptance:** L M12 `:196` gives the eventual pilot a
visible row. D E12 leaves it a separately authorized step without a build row.
Preserve an explicit gated post-v1 pilot row, after synthetic evidence and Jason's
source/access/content approvals. Replace a fixed week/server-count comparison with
identity-level reconciliation, crash/outage/revoke cases and a defined coverage target.
Q210 favors immediate authorized progress, not a mandatory calendar wait.
## 3. Della strengths missing or materially weaker in Ludwig
- **K1 Grant lifecycle:** D `:212-235,290` separates source credentials from mediated
access, enforces human grant approval, expiry, revoke races and manual mode (F2).
- **K2 Crash-safe work:** D `:131-156,291` defines leases, cursor/outbox atomicity,
recipient receipts, bounded reconciliation and separate replay generations (F3/F4).
- **K3 Data minimization:** D `:176-199` uses metadata-only baseline, explicit fetch,
encrypted scoped storage/cursors, keyed fingerprints, backup expiry, holds and
deletion authority. L's always-raw plaintext store does not provide these controls.
- **K4 Resource bounds:** D `:138-146,158-164` names slice, retained-byte, pending-work,
MIME-depth/part/text bounds and holds under audit/disk failure. L names a raw-size cap
but leaves per-run limits unspecified and parser expansion/cumulative quotas uncovered.
- **K5 Manager contract:** D `:260-277,298` includes one retry owner, source/business
scope, synthetic canary, uncertain-effect fencing, manager batches and human-kill
persistence. Keep these and add Ludwig's explicit repeated-exit escalation.
- **K6 Security acceptance:** D E02/E08/E09/E11 `:290,296,297,299` tests hand grants,
stale approval, manual mode, revoke/in-flight races, cross-business access and backup
restore. L's cross-seat test and reduced tool list do not exercise these boundaries.
## 4. Disagreements and recommended resolution
| Ref | Difference | Side and reason |
| --- | --- | --- |
| D1 | Graph first in D E05 vs IMAP first in L M4 | Choose the first approved pilot source, not a protocol mandate. Prefer IMAP for a personally controlled IMAP mailbox. Prefer Graph if the approved mailbox is M365. Both remain post-v1 adapters. Reusing design does not require retaining USC infrastructure. |
| D2 | D aliases first vs L plus-addressing first | Take D's requirement for provider-verified original-recipient evidence. Choose aliases or plus-tags only after that proof and Jason's address ruling. Neither is an access boundary. |
| D3 | D metadata-only/explicit content vs L stores every raw message/attachment | Take D's default and scoped encryption. Raw retention is an explicit optional approved policy, not unconditional intake. Keep raw-fidelity fixtures to test parsers without retaining every live message. |
| D4 | D 30-day bodies/drafts, 7-day attachments vs L 90 days for all content | Recommend D's shorter pilot limits, 365-day minimal evidence in both. Jason decides business retention/holds. Add quotas and backup expiry rather than relying on retention days alone. |
| D5 | D scanner before isolated extraction vs L no scanner initially | Start with metadata only. Fetched bytes stay quarantined. Require isolated extraction and scanning before model-visible extracted content, or explicitly limit the first release to opaque retrieval without extraction. A read-only workspace bind does not stop code from reading/executing bytes. |
| D6 | D minimal explicitly permitted content vs L metadata-only model classification | Take L's metadata-only baseline, corrected by F7. Both need an approved mail decision schema and useful-result fixtures. A later content field needs explicit approval, not an alternate decision client. |
| D7 | D optional POP3 row vs L no POP3 build | Take L's deferral until Jason names a POP-only need. Preserve the compatibility boundary and safe UIDL/no-DELE requirements as a documented future condition, not a committed implementation row. |
| D8 | D ordinary governed agent work vs L restricted mail-turn authority | Take L's explicit restriction with G1's fresh-context and propagation conditions. Preserve a human-approved route for later promotion to ordinary work. |
| D9 | L's instance-secret slot vs D's dataRoot slot example | Take L's config-directory placement, with an explicit new service-principal schema. D `:233` is wrong against S `packages/business/src/credentials.mjs:95-102`, which forbids token files in dataRoot. Correct that example in phase C, not the validator. S `packages/bus/src/credentials.mjs:216-239` also constrains loaded locations. |
Neither interface spelling wins on safety. In phase C choose one naming scheme and
declare exact schemas once. Add descriptor/capability discovery to L's source exports.
Keep broker-mediated tools identical across harnesses, local-only drafts and no send.
Neither plan's later live row is authorization to connect now.
## 5. Merged Jason questions (no duplicate decisions)
These replace, rather than append to, both original question lists. Origin labels
refer to the original question numbers, not the new numbers below.
1. **Q1 Pilot provider/domain/addresses:** which controlled domain, provider and agent
ingress mailbox should be enrolled, and aliases or plus-tags? **Recommend** one
approved non-USC agent mailbox, no catch-all/MX change, with proven recipient
preservation. Build its protocol first. Dedicated mailboxes only for an approved
isolation need. (L Q1/Q2, D Q1.)
2. **Q2 Personal mail scope:** which existing user mailbox, if any, may be read?
**Recommend none initially**. Later enrollment names the user/mailbox and exact
consent/delegation, separately from agent ingress. (L Q8, D Q1/Q2 personal scope.)
3. **Q3 Source mutation:** should intake mark read, flag, label, move or save drafts
into the provider? **Recommend no**, read-only source access and local drafts.
(L Q3 and provider-draft portion of Q4.)
4. **Q4 Service/access grants:** approve a protected mail-service credential owner and
distinct per-agent/user access grants under the chief/human approval lifecycle?
**Recommend yes**, via reviewed post-v1 PRD/vocabulary changes, a new mail-access
hard-wait action distinct from provider credential creation, and exact targets,
rights, expiry and immediate local revoke. (D Q2.)
5. **Q5 Unattended wake:** which senders/routes may wake agents without per-message
approval? **Recommend** explicit sender allowlist plus trusted aligned authentication,
approved route/access grant, restricted mail-turn profile and manager admission.
Manual mode overrides it. Authentication failures/ambiguity quarantine for review.
None of this approves external effects or user decisions. (L Q5, D Q3.)
6. **Q6 Content/retention/deletion:** what content may be retained, for how long, and
what holds apply? **Recommend** encrypted 30-day body/draft, 7-day explicitly fetched
attachments, 365-day minimal evidence/tombstones, metadata-only until approved.
Preserve exact `data.delete` authorization, backup expiry and restore suppression.
(L Q6, D Q4.)
7. **Q7 Dependencies:** accept maintained IMAP/MIME dependencies after review and exact
version pins rather than custom protocol/parser implementations? **Recommend yes**,
assess Ludwig's named candidates during their owning rows. This is routine technical
review unless repository policy adds a dependency approval gate. (L Q7.)
8. **Q8 Attachment handling:** may attachment bytes be fetched/extracted, and is a
scanner required? **Recommend metadata-only first**, explicit quarantined fetch,
isolated no-network extraction plus scanning before exposing extracted content.
Do not enable arbitrary attachment execution or automatic archive expansion. (L Q9.)
9. **Q9 New exposure/spend:** authorize any new listener/webhook, DNS/MX change, paid
infrastructure or model spend? **Recommend none initially**. Outbound monitoring
and existing internal notifications suffice. Each later change has its own approval.
(D Q5, overlapping listener boundary in L Q2.)
10. **Q10 Sending:** should this add-on ever send/forward/reply? **Recommend no**.
R21 remains for the specialist. A future separate sender needs its own approved
plan and exact-recipient human approval through `message.external`. (L Q4, D Q6.)
11. **Q11 Prior repositories/service:** archive the older repo or change any existing
USC service? **Recommend no operational change in this assignment**. Keep both
histories. Treat MP as the consolidated design reference. Any archival or service
retirement is a separate authorized task after verifying actual deployment state,
which this review did not inspect. (L Q10.)
## Cross-review reconciliation received before publication
Ludwig's `ludwig-0002` arrived during finalization. Read all 102 lines at Stack
`143415779:docs/plans/2026-10-21_email-review-by-ludwig.md` (LR). The original plans
remain unchanged. These are phase C recommendations, not changes to their text.
- **C1 Agreed corrections:** LR `:12-19` identifies D's duplicate-check line nit,
invalid dataRoot credential slot and overbroad `credential.mint` wording. Correct
them in phase C. This review's F2 and Q4 now explicitly distinguish access rights
from provider credential issuance. LR `:62` recommends no agent reroute. Adopt that
initial tool boundary: human-approved address rules route intake, and agents cannot
widen readership by rerouting a message. Keep any later reroute/delegation design
separate, with exact access authorization and no content laundering through bus text.
- **C2 Shared graft:** LR `:26-39,63-72` agrees with most durability, grant, retention,
quota, provider-order and attachment controls above. Its secret-scan result may be
included only as Ludwig's attributed, pattern-based finding, not my independent scan
and not proof of absence or that no rotation could be needed (F10).
- **C3 Keep qualifications:** LR `:65` says key deletion is the only way expiry reaches
backups. That is too absolute: a design may delete backup copies or use crypto-erasure,
but crypto-erasure works only if all relevant key copies/escrows expire too. Specify
the actual restoration/expiry controls. LR `:67` must not imply aliases avoid delivery
provenance proof. Both aliases and plus-addressing require that proof (F9/D2).
## Review receipt
Read Ludwig's complete pinned plan, compared both proposals and checked the cited
implementation/PRD/manager passages. Confirmed repository ancestry and equal source/test
trees. Public Graph documentation was checked for F5 on 2026-10-21. No live mailbox,
secret file, service, implementation, dependency or MVP/queue change occurred. Canonical
`queue next della` returned `nothing`. Static checks cover review structure, cited
line existence, question-source coverage and unchanged original plan blob. No product
suite or live validation was run. Commit/push this review on `della/email-plan`, then
deliver della-0002 immediately to Mos and Ludwig under Q210. Phase C remains Mos's call.