From fb65cc0b30cc9aabb766915cd25e253b4376e2b8 Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Tue, 16 Jun 2026 20:25:18 +0000 Subject: [PATCH 1/9] feat: multi-domain support (cloudflare/agentic-inbox#49) Backport PR #49: parseDomains() helper + mailbox domain guard so one instance serves multiple domains via comma-separated DOMAINS. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 22 ++++++++++++++++++++-- package.json | 2 +- workers/index.ts | 20 +++++++++++++++++--- wrangler.jsonc | 6 ++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e1e2eb72..ec6ec412 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ https://github.com/cloudflare/agentic-inbox/issues/4#issuecomment-4269118513 ### To set up -1. Deploy to Cloudflare. The deploy flow will automatically provision R2, Durable Objects, and Workers AI. You'll be prompted for **DOMAINS**, which is the domain (yourdomain.com) you want to receive emails for (email@yourdomain.com). +1. Deploy to Cloudflare. The deploy flow will automatically provision R2, Durable Objects, and Workers AI. You'll be prompted for **DOMAINS**, which is the domain (yourdomain.com) you want to receive emails for (email@yourdomain.com). To serve more than one domain from a single instance, pass a comma-separated list (e.g. `yourdomain.com,anotherdomain.com`) — see [Using multiple domains](#using-multiple-domains). [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/agentic-inbox) @@ -59,9 +59,27 @@ npm run dev ### Configuration -1. Set your domain in `wrangler.jsonc` +1. Set your domain (or domains) in `wrangler.jsonc` via the `DOMAINS` var 2. Create an R2 bucket named `agentic-inbox`: `wrangler r2 bucket create agentic-inbox` +### Using multiple domains + +A single instance can serve multiple domains. Set `DOMAINS` to a comma-separated list: + +```jsonc +"DOMAINS": "example.com,another.com" +``` + +Then, for **each** domain: + +- Add a catch-all [Email Routing](https://developers.cloudflare.com/email-routing/) rule that forwards to this Worker (for receiving) +- Verify the domain for [Email Service](https://developers.cloudflare.com/email-service/) (for sending) + +Notes: + +- The **New Mailbox** dialog shows a domain picker automatically once more than one domain is configured; mailbox creation is restricted to the configured domains. +- If you set `EMAIL_ADDRESSES` to restrict mailbox creation, it may list addresses across any of the configured domains (e.g. `["hello@example.com", "hi@another.com"]`). + ### Deploy ```bash diff --git a/package.json b/package.json index 9d6f0272..311d4064 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "products": ["Workers", "Durable Objects", "R2", "Workers AI"], "bindings": { "DOMAINS": { - "description": "Your domain with [Email Routing](https://developers.cloudflare.com/email-routing/) enabled (e.g. `example.com`). After deploying, create a catch-all Email Routing rule pointing to this Worker." + "description": "Your domain with [Email Routing](https://developers.cloudflare.com/email-routing/) enabled (e.g. `example.com`). For multiple domains, pass a comma-separated list (e.g. `example.com,another.com`). After deploying, create a catch-all Email Routing rule pointing to this Worker for each domain." } } }, diff --git a/workers/index.ts b/workers/index.ts index fd3359ce..4301fe49 100644 --- a/workers/index.ts +++ b/workers/index.ts @@ -50,6 +50,12 @@ function slugify(text: string) { // can return "" for non-alphanumeric input .replace(/--+/g, "-").replace(/^-+/, "").replace(/-+$/, ""); } +// Parse the comma-separated DOMAINS var into a trimmed, non-empty list. +// Supports multiple domains on one instance, e.g. "example.com,another.com". +function parseDomains(raw: string | undefined): string[] { + return (raw || "").split(",").map((d) => d.trim()).filter(Boolean); +} + function intQuery(c: AppContext, key: string): number | undefined { const v = c.req.query(key); if (!v) return undefined; @@ -86,8 +92,7 @@ app.use("/api/v1/mailboxes/:mailboxId/*", requireMailbox); // -- Config --------------------------------------------------------- app.get("/api/v1/config", (c) => { - const domainsRaw = c.env.DOMAINS || ""; - const domains = domainsRaw.split(",").map((d) => d.trim()).filter(Boolean); + const domains = parseDomains(c.env.DOMAINS); const emailAddresses = c.env.EMAIL_ADDRESSES ?? []; return c.json({ domains, emailAddresses }); }); @@ -103,9 +108,18 @@ app.post("/api/v1/mailboxes", async (c) => { const { name, settings, email: rawEmail } = CreateMailboxBody.parse(await c.req.json()); const email = rawEmail.toLowerCase(); const allowedAddresses = (c.env.EMAIL_ADDRESSES ?? []) as string[]; - if (allowedAddresses.length > 0 && !allowedAddresses.map((a) => a.toLowerCase()).includes(email)) { + const isExplicitlyAllowed = allowedAddresses.map((a) => a.toLowerCase()).includes(email); + if (allowedAddresses.length > 0 && !isExplicitlyAllowed) { return c.json({ error: "Mailbox creation is restricted to configured EMAIL_ADDRESSES" }, 403); } + // When DOMAINS is configured, mailboxes must be on one of those domains — this mirrors the + // front-end domain picker. Explicit EMAIL_ADDRESSES entries bypass the check (they are the + // authoritative allow-list and may legitimately span domains). + const domains = parseDomains(c.env.DOMAINS); + const domain = email.split("@")[1]; + if (!isExplicitlyAllowed && domains.length > 0 && (!domain || !domains.some((d) => d.toLowerCase() === domain))) { + return c.json({ error: "Mailbox domain must be one of the configured DOMAINS" }, 400); + } const key = `mailboxes/${email}.json`; if (await c.env.BUCKET.head(key)) return c.json({ error: "Mailbox already exists" }, 409); const defaultSettings = { fromName: name, forwarding: { enabled: false, email: "" }, signature: { enabled: false, text: "" }, autoReply: { enabled: false, subject: "", message: "" } }; diff --git a/wrangler.jsonc b/wrangler.jsonc index 53a65cd0..98300c5b 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -13,7 +13,13 @@ // Production deploys must also define POLICY_AUD and TEAM_DOMAIN. // TEAM_DOMAIN may be the base Access URL or the full /cdn-cgi/access/certs URL. // The worker now fails closed outside local development if Access is not configured. + // + // DOMAINS accepts a single domain or a comma-separated list to serve multiple + // domains from one instance, e.g. "example.com,another.com". Each domain needs its + // own Email Routing catch-all rule, and must be verified for outbound sending. "DOMAINS": "example.com", + // EMAIL_ADDRESSES optionally restricts mailbox creation to specific addresses, and + // may span the configured domains, e.g. ["hello@example.com", "hi@another.com"]. "EMAIL_ADDRESSES": [] }, "send_email": [ From 47ef68254aef4cc78db899fc66805a3a88c6e0b6 Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Tue, 16 Jun 2026 20:30:25 +0000 Subject: [PATCH 2/9] feat: bridge integration (webhooks + sync send, disable built-in AI) - notifyBridge() helper: fire-and-forget webhook to agentic-inbox-bridge, no-ops when WEBHOOK_URL unset (vanilla upstream behaviour preserved). - receiveEmail(): replace built-in Workers AI auto-draft trigger with an email-received webhook (Hermes is the sole draft generator). - POST /emails: add ?sync=true (bridge approve path, blocks until delivery, 502 on failure); async/web-UI path fires email-sent webhook for dedup. - types.ts: optional WEBHOOK_URL/WEBHOOK_SECRET on Env (secret set via wrangler secret put, not committed). Refinement vs plan: email-sent webhook fires on the async (web-UI) path only, not the sync path, so the bridge does not receive an echo of its own sends. Co-Authored-By: Claude Opus 4.8 (1M context) --- .dev.vars.example | 5 ++++ workers/index.ts | 63 +++++++++++++++++++++++++++++++++++------- workers/lib/webhook.ts | 27 ++++++++++++++++++ workers/types.ts | 5 ++++ wrangler.jsonc | 4 +++ 5 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 workers/lib/webhook.ts diff --git a/.dev.vars.example b/.dev.vars.example index f0c881b3..3adb5081 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -1,3 +1,8 @@ # Cloudflare Access (required in production). TEAM_DOMAIN may be the base Access URL or the full /cdn-cgi/access/certs URL. POLICY_AUD=your-access-policy-audience-tag TEAM_DOMAIN=https://your-team.cloudflareaccess.com + +# Nimblersoft bridge integration (agentic-inbox-bridge). Optional for local dev. +# WEBHOOK_URL also lives in wrangler.jsonc vars; WEBHOOK_SECRET is a Worker secret in prod. +WEBHOOK_URL=https://mail-bridge.nimblersoft.com/webhooks/agentic-inbox +WEBHOOK_SECRET=local-dev-shared-secret diff --git a/workers/index.ts b/workers/index.ts index 4301fe49..9764f8b6 100644 --- a/workers/index.ts +++ b/workers/index.ts @@ -16,6 +16,7 @@ import { listMailboxes, } from "./lib/email-helpers"; import { SendEmailRequestSchema } from "./lib/schemas"; +import { notifyBridge } from "./lib/webhook"; import { handleReplyEmail, handleForwardEmail } from "./routes/reply-forward"; import { Folders } from "../shared/folders"; import type { Env } from "./types"; @@ -215,12 +216,39 @@ app.post("/api/v1/mailboxes/:mailboxId/emails", async (c: AppContext) => { ]), }, attachmentData); + const send = sendEmail(c.env.EMAIL, { + to, cc, bcc, from, subject, html, text, + attachments: attachments?.map((att) => ({ content: att.content, filename: att.filename, type: att.type, disposition: att.disposition || "attachment", contentId: att.contentId })), + ...(in_reply_to ? { headers: buildThreadingHeaders(in_reply_to, references || []) } : {}), + }); + + // `?sync=true` (the bridge's approve path) blocks until delivery so the caller + // gets a definitive result in the HTTP response — no email-sent webhook is fired + // because the caller already knows it sent. The default async path (web UI) + // returns immediately and notifies the bridge via the email-sent webhook so it + // can dedup against a pending MM approval. + if (boolQuery(c, "sync") === true) { + try { + await send; + } catch (e) { + return c.json({ error: "Email delivery failed", detail: (e as Error).message }, 502); + } + return c.json({ id: messageId, status: "sent" }, 200); + } + c.executionCtx.waitUntil( - sendEmail(c.env.EMAIL, { - to, cc, bcc, from, subject, html, text, - attachments: attachments?.map((att) => ({ content: att.content, filename: att.filename, type: att.type, disposition: att.disposition || "attachment", contentId: att.contentId })), - ...(in_reply_to ? { headers: buildThreadingHeaders(in_reply_to, references || []) } : {}), - }).catch((e) => console.error("Deferred email delivery failed:", (e as Error).message)), + send + .then(() => notifyBridge(c.env, { + event: "email-sent", + mailboxId, + emailId: messageId, + threadId: thread_id || in_reply_to || messageId, + to: toStr, + subject, + source: "web-ui", + timestamp: new Date().toISOString(), + })) + .catch((e) => console.error("Deferred email delivery failed:", (e as Error).message)), ); return c.json({ id: messageId, status: "sent" }, 202); }); @@ -416,11 +444,26 @@ async function receiveEmail(event: { raw: ReadableStream; rawSize: number }, env thread_id: threadId, message_id: originalMessageId, raw_headers: JSON.stringify(parsedEmail.headers), }, attachmentData); - const agentStub = env.EMAIL_AGENT.get(env.EMAIL_AGENT.idFromName(mailboxId)); - ctx.waitUntil(agentStub.fetch(new Request("https://agents/onNewEmail", { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ mailboxId, emailId: messageId, sender: (parsedEmail.from?.address || "").toLowerCase(), subject: parsedEmail.subject || "", threadId }), - })).catch((e) => console.error("Auto-draft trigger failed:", (e as Error).message))); + // Nimblersoft: the built-in Workers AI auto-draft is disabled — Hermes is the sole + // draft generator. Instead of triggering EMAIL_AGENT, fire a reference-only + // email-received webhook to the bridge, which fetches the full body via the API + // and runs the persona reply through Hermes /v1/responses. + ctx.waitUntil(notifyBridge(env, { + event: "email-received", + mailboxId, + emailId: messageId, + from: (parsedEmail.from?.address || "").toLowerCase(), + fromName: parsedEmail.from?.name || "", + to: allRecipients, + cc: ccRecipients, + subject: parsedEmail.subject || "", + threadId, + inReplyTo, + messageId: originalMessageId, + hasAttachments: attachmentData.length > 0, + attachmentCount: attachmentData.length, + timestamp: new Date().toISOString(), + })); } export { app, receiveEmail }; diff --git a/workers/lib/webhook.ts b/workers/lib/webhook.ts new file mode 100644 index 00000000..6e09599d --- /dev/null +++ b/workers/lib/webhook.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +import type { Env } from "../types"; + +// Fire-and-forget notifier to the external Nimblersoft bridge +// (agentic-inbox-bridge). No-ops when WEBHOOK_URL is unset, so local dev and +// vanilla upstream deploys are unaffected. The shared X-Webhook-Secret lets the +// bridge authenticate the caller. Errors are logged, never thrown — callers +// wrap this in ctx.waitUntil() and must not let webhook delivery break mail flow. +export async function notifyBridge(env: Env, payload: Record): Promise { + if (!env.WEBHOOK_URL) return; + try { + const res = await fetch(env.WEBHOOK_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(env.WEBHOOK_SECRET ? { "X-Webhook-Secret": env.WEBHOOK_SECRET } : {}), + }, + body: JSON.stringify(payload), + }); + if (!res.ok) console.error(`Bridge webhook ${payload.event} failed: ${res.status}`); + } catch (e) { + console.error(`Bridge webhook ${payload.event} error:`, (e as Error).message); + } +} diff --git a/workers/types.ts b/workers/types.ts index c8966727..25c7a1f7 100644 --- a/workers/types.ts +++ b/workers/types.ts @@ -5,4 +5,9 @@ export interface Env extends Cloudflare.Env { POLICY_AUD: string; TEAM_DOMAIN: string; + // Nimblersoft bridge integration (agentic-inbox-bridge). Optional: when unset, + // webhook emission is skipped (vanilla upstream behaviour). WEBHOOK_SECRET is a + // Worker secret (`wrangler secret put WEBHOOK_SECRET`), not a committed var. + WEBHOOK_URL?: string; + WEBHOOK_SECRET?: string; } diff --git a/wrangler.jsonc b/wrangler.jsonc index 98300c5b..7d9838c2 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -14,6 +14,10 @@ // TEAM_DOMAIN may be the base Access URL or the full /cdn-cgi/access/certs URL. // The worker now fails closed outside local development if Access is not configured. // + // Nimblersoft bridge integration (optional, same pattern as POLICY_AUD): set + // WEBHOOK_URL as a var and WEBHOOK_SECRET as a secret in production — + // `wrangler secret put WEBHOOK_SECRET`. Leave both unset to disable webhooks. + // // DOMAINS accepts a single domain or a comma-separated list to serve multiple // domains from one instance, e.g. "example.com,another.com". Each domain needs its // own Email Routing catch-all rule, and must be verified for outbound sending. From 6081e12cb2a252a152cd58ea5dae5bdc7fcf5284 Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Tue, 16 Jun 2026 20:31:28 +0000 Subject: [PATCH 3/9] docs: scaffold project AGENTS.md, CONTEXT.md, PLAN.md Self-contained project context: fork modifications, config/secrets pattern, known live-infra watch-outs, project glossary, and the migration plan copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 84 +++++++++++ CONTEXT.md | 26 ++++ PLAN.md | 430 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 540 insertions(+) create mode 100644 AGENTS.md create mode 100644 CONTEXT.md create mode 100644 PLAN.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..df398790 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,84 @@ +# AGENTS.md — agentic-inbox (Nimblersoft fork) + +Working context for any agent operating on this project. Company-wide context lives in +`~/nimbler-ops/AGENTS.md` (canonical project inventory) and `~/nimbler-ops/CONTEXT.md` (glossary). + +## What this is + +Self-hosted email for AI agents, replacing OpenMail. Fork of +[`cloudflare/agentic-inbox`](https://github.com/cloudflare/agentic-inbox) running on Cloudflare +Workers + Durable Objects (SQLite per mailbox) + R2 (attachments) + Email Routing + Email Service. +Deployed at `https://ai.nimblersoft.com`. + +- **Repo:** `ericmaster/agentic-inbox`, branch `feat/nimblersoft-multidomain-bridge`. +- **Upstream base:** clean fork (last upstream commit `48039bb`, Merge PR #7). +- **Migration plan:** see [`PLAN.md`](PLAN.md). Glossary in [`CONTEXT.md`](CONTEXT.md). + +## Fork modifications (2 commits on top of upstream) + +### Commit 1 — `fb65cc0` multi-domain support (backport of upstream PR #49) +- `parseDomains()` helper in `workers/index.ts`; `/api/v1/config` uses it. +- Mailbox creation guard: when `DOMAINS` is set, a new mailbox's domain must be one of them + (explicit `EMAIL_ADDRESSES` entries bypass the check). One instance serves + `ai.nimblersoft.com` + others via comma-separated `DOMAINS`. +- Docs: `README.md`, `package.json`, `wrangler.jsonc` comments. + +### Commit 2 — `47ef682` bridge integration + disable built-in AI +- **`workers/lib/webhook.ts`** — `notifyBridge(env, payload)`: fire-and-forget POST to the + bridge with `X-Webhook-Secret`. **No-ops when `WEBHOOK_URL` is unset** → vanilla upstream + behaviour is preserved; errors are logged, never thrown. +- **`receiveEmail()`** — the built-in Workers AI auto-draft trigger (`EMAIL_AGENT.fetch`) is + **removed**. Hermes is the sole draft generator. Instead we fire a reference-only + `email-received` webhook; the bridge fetches the full body via the API. +- **`POST /api/v1/mailboxes/:id/emails`** — added `?sync=true`: + - `sync=true` (bridge approve path): `await sendEmail`, return `200 {status:"sent"}`, + `502` on delivery failure. **No** `email-sent` webhook — the caller already has the result. + - default (web UI): `waitUntil(sendEmail → email-sent webhook)`, return `202`. +- `workers/types.ts` — optional `WEBHOOK_URL?` / `WEBHOOK_SECRET?` on `Env`. + +> **Deliberate deviation from PLAN.md's fork table:** the plan listed the `email-sent` webhook as +> firing "after successful sync send". It fires on the **async (web-UI) path only**. Firing it on +> the bridge's own sync send would echo the bridge's sends back to it and break dedup. + +### Webhook payload contracts +See PLAN.md → "Webhook Payload Contract". `email-received` is reference-only (bridge fetches the +body via `GET /api/v1/mailboxes/:mailboxId/emails/:emailId`). `email-sent` carries `source:"web-ui"`. + +## Config & secrets + +Same pattern as upstream's `POLICY_AUD`/`TEAM_DOMAIN` (declared in `types.ts`, set in prod — NOT +committed to `wrangler.jsonc`): + +| Key | Where | Notes | +|---|---|---| +| `DOMAINS` | `wrangler.jsonc` var | `ai.nimblersoft.com,ericmaster.ninja,meliruns.com` — but only `ai.nimblersoft.com` is cut over in Phase 1 (the other two are live on Zoho; deferred to PLAN Phase 4). | +| `EMAIL_ADDRESSES` | `wrangler.jsonc` var | optional allow-list of creatable mailbox addresses. | +| `POLICY_AUD`, `TEAM_DOMAIN` | secret/var (Infisical) | Cloudflare Access. | +| `WEBHOOK_URL` | var (Infisical) | `https://mail-bridge.nimblersoft.com/webhooks/agentic-inbox`. | +| `WEBHOOK_SECRET` | **Worker secret** | `wrangler secret put WEBHOOK_SECRET`. Shared with the bridge. | + +Infisical project: **Agentic Inbox** (`6c293d88-cc3c-4a44-8368-faa22c8c0196`). `.dev.vars.example` +documents local-dev values. Never commit real secrets. + +## Commands + +```bash +npm install +npm run typecheck # wrangler types + tsc --noEmit +npm run build # react-router + vite build +npm run dev # local dev server (needs .dev.vars) +npm run deploy # wrangler deploy — DO NOT run unsupervised (live infra) +``` + +## Known issues / watch-outs + +- **Live infra steps are human-gated.** Deploy, DNS/MX cutover, Email Routing, Email Service + verification, and Infisical secret writes are NOT done autonomously. See PLAN.md Phase 1. +- `ai.nimblersoft.com` is a **single-MX hard cutover** (currently Mailgun/OpenMail). Validate on a + test address before flipping; rollback = repoint MX to Mailgun. +- **Do not** enable Email Routing/catch-all on `ericmaster.ninja` or `meliruns.com` before PLAN + Phase 4 — both are live on Zoho and would have their inbound hijacked. +- `nimblersoft.com` MX (Google Workspace) must never be touched. +- The bridge itself (`~/agentic/agentic-inbox-bridge/`) is built in PLAN Phase 2; this repo only + emits webhooks to it. +- Generated `worker-configuration.d.ts` and `build/` are gitignored. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..93ba7ebe --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,26 @@ +# CONTEXT.md — agentic-inbox glossary + +Project-local terminology. Defers to `~/nimbler-ops/CONTEXT.md` for company-wide terms; only +project-specific language is defined here. Use these terms verbatim. + +- **Agentic Inbox** — this service. Self-hosted email app for AI agents on Cloudflare Workers, + fork of `cloudflare/agentic-inbox`. Reachable at `ai.nimblersoft.com`. +- **Bridge** — `agentic-inbox-bridge` (`~/agentic/agentic-inbox-bridge/`, PLAN Phase 2). The + Hono/TypeScript daemon that connects Agentic Inbox ↔ Mattermost ↔ Hermes. This repo only emits + webhooks to it; it is not built here. +- **Mailbox** — a Durable Object (SQLite) keyed by full email address (e.g. + `sofia.luz@ai.nimblersoft.com`). Holds that address's emails, folders, settings. +- **email-received webhook** — reference-only event Agentic Inbox fires to the bridge when mail + arrives. Carries IDs/metadata, not the body; the bridge fetches the body via the API. +- **email-sent webhook** — event fired on the **async (web-UI) send path** so the bridge can dedup + against a pending Mattermost approval. `source:"web-ui"`. Not fired on the sync path. +- **sync send** — `POST /emails?sync=true`: blocks until delivery and returns the result inline. + The bridge's "approve" path uses this; it does not produce an email-sent webhook. +- **Hermes** — Nimblersoft's agent platform; sole reply-draft generator (`/v1/responses`). The + built-in Workers AI auto-draft is disabled in this fork. +- **Persona** — a Hermes agent identity bound to a mailbox (e.g. Sofia → `sofia.luz@`, + Silas → `silas.vertiz@`). +- **DOMAINS** — comma-separated list of domains one instance serves. Listing a domain ≠ cutting it + over; cutover = pointing its MX/Email Routing at the Worker. +- **OpenMail** — the SaaS email provider being replaced (`api.openmail.sh`, Mailgun-backed). The + legacy bridge is `~/agentic/hermes-mail-mm-bridge/` (archived in PLAN Phase 3). diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..5dcb5418 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,430 @@ +# Implementation Plan: OpenMail → Agentic Inbox Migration + +## Overview +Replace `openmail.sh` (SaaS email provider) with `agentic-inbox` self-hosted on Cloudflare Workers at `ai.nimblersoft.com`. Phase 1 cuts over `ai.nimblersoft.com` only; the additional domains (`ericmaster.ninja`, `meliruns.com`) are **live on Zoho** and their cutover is deferred to Phase 4 alongside the Zoho migration. `nimblersoft.com` stays on Google Workspace (MX records untouched, no forwarding). Preserve Hermes Mattermost notification + Hermes persona reply approval workflow. Decommission all OpenMail artifacts. Migrate select Zoho mail data (low priority, deferred). + +## Current Context +- **Current provider:** OpenMail (`api.openmail.sh`) — WebSocket-based, CLI tool `@openmail/cli` +- **Existing accounts:** sofia.luz@ai.nimblersoft.com, silas.vertiz@ai.nimblersoft.com +- **Bridge:** `~/agentic/hermes-mail-mm-bridge/` — Node.js daemon (WS → MM REST → Hermes `/v1/responses`) +- **Bridge features:** per-agent routing, approval workflow (✅ reactions via polling), correction detection (10s poll), owner auto-send, CC cross-inbox routing, stateful `conversation:` threading via `/v1/responses` +- **Target:** `ericmaster/agentic-inbox` (fork of `cloudflare/agentic-inbox`, clean — 0 diff) +- **Target architecture:** Cloudflare Workers + Durable Objects (SQLite per mailbox) + R2 (attachments) + Email Routing + Workers AI agent +- **Multi-domain PR:** cloudflare/agentic-inbox#49 — not yet merged upstream. Patches ready to apply to fork. + +## Architecture Change + +### Current (OpenMail) +``` +External Email → OpenMail Provider → WebSocket push → bridge.js (polls MM every 5s/10s) + → Mattermost notification → Hermes /v1/responses → suggested reply + → admin ✅ reaction → poll detects → bridge.js → OpenMail CLI send +``` + +### Target (Agentic Inbox) +``` +External Email → CF Email Routing → Agentic Inbox Worker → Durable Object (inbox) + → fire-and-forget webhook → POST to bridge (reference only, no body) + → bridge fetches email via API → Mattermost notification + → Hermes /v1/responses → suggested reply → posted as threaded reply + → admin types "approve" in thread → MM outgoing webhook → bridge sends via API (?sync=true) + → (parallel) web UI approval → Agentic Inbox sends → email-sent webhook → bridge dedup + → corrections: any non-bot reply in thread → MM outgoing webhook → bridge revises via Hermes +``` + +## Decisions (Phase 0 — Grilling Session) + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Inbox domain** | `ai.nimblersoft.com` (NOT nimblersoft.com) | nimblersoft.com MX → Google Workspace. Cannot move without breaking Gmail. | +| **nimblersoft.com email** | No change, no forwarding | Don't mix human Gmail mailboxes with AI agent mailboxes. | +| **MM notifications** | Keep | Know when agent receives email and prepares to send. | +| **Approval flow** | Exact-match "approve" reply + web UI | Zero polling. MM outgoing webhook delivers all channel posts to bridge. Bridge filters for "approve" (case-insensitive, trimmed, must be reply to known pending thread). Web UI approval coexists with dedup. | +| **Correction detection** | MM outgoing webhook (no trigger word) | Outgoing webhook fires on every post in channel. Bridge filters: non-bot reply to known root_post_id = correction. Zero polling. | +| **Hermes personas** | Keep — same integration path | Agent personas (Sofia/Silas) require context-aware replies via `/v1/responses`. | +| **AI strategy** | Hermes sole draft generator; disable Agentic Inbox built-in AI | Workers AI drafts are generic. Remove `agentStub.fetch()` auto-draft trigger in fork. | +| **Dual-approval dedup** | Agentic Inbox emits `email-sent` webhook | When web UI sends, Agentic Inbox fires webhook → bridge marks pending resolved → updates MM post with audit trail. | +| **Conversation keying** | `agentic-inbox-{mailbox_address}-{thread_id}` | Mailbox-scoped. Prevents cross-mailbox context bleed. | +| **CC routing** | Drop CC parsing (Scenario A) | Modern mail servers deliver separate copies to each recipient. Each inbox processes independently. | +| **Self-sent loop detection** | Keep same logic | If sender matches a configured mailbox address → notification only, no draft. | +| **Bridge deployment** | Docker + CF Tunnel at `mail-bridge.nimblersoft.com` | Same pattern as `hermes-mm-bridge` and `hermes-plane-bridge`. Defer CF Workers deployment until Hermes private access pattern is solved. | +| **Bridge tech stack** | Hono + TypeScript | Aligned with Agentic Inbox. Lightweight, type-safe. | +| **Bridge API auth** | CF Access Service Token | No code changes in Agentic Inbox. Access validates at edge, injects JWT. Bridge sends `CF-Access-Client-Id` + `CF-Access-Client-Secret` headers. | +| **Webhook auth** | Shared secrets | `X-Webhook-Secret` header validated against env vars for both Agentic Inbox and MM webhooks. Stored in Infisical. | +| **Webhook payload** | Reference only (no body) | Webhook sends `{ mailboxId, emailId, from, subject, threadId, ... }`. Bridge fetches full email via `GET /api/v1/mailboxes/:mailboxId/emails/:emailId`. | +| **Send API** | Use existing `POST /api/v1/mailboxes/:mailboxId/emails` | Already exists in upstream. Add `?sync=true` query param for synchronous mode (bridge). Default async for web UI. | +| **Email signatures** | Agentic Inbox per-mailbox settings | Signatures stored in Agentic Inbox settings, appended on send. Hermes system prompt: "No generes firma — se agrega automáticamente al enviar." | +| **Restart recovery** | Startup reconciliation | Bridge scans recent MM posts (last 24h) on boot, finds unprocessed "approve" replies. ~20 lines. | +| **MCP** | REST API for bridge; MCP available for other tools | MCP out of scope for this plan. | +| **Zoho migration** | Partial, low priority, deferred | Only specific recent emails. Defer as long as agentic-inbox receives new mail reliably. | + +## Fork Modifications (`ericmaster/agentic-inbox`) + +Two commits on the fork: + +### Commit 1: Multi-domain support (PR #49 patches, ~44 lines) +- `workers/index.ts` — `parseDomains()` helper + backend guard +- `wrangler.jsonc` — multi-domain comments +- `README.md` — documentation +- `package.json` — deployment description update + +### Commit 2: Bridge integration (~38 lines) +| File | Change | Lines | +|------|--------|-------| +| `workers/index.ts` (`receiveEmail()`) | Add fire-and-forget webhook to bridge after `stub.createEmail()` | ~10 | +| `workers/index.ts` (`receiveEmail()`) | Remove `agentStub.fetch()` auto-draft trigger | ~5 (delete) | +| `workers/index.ts` (`POST /emails`) | Add `?sync=true` — `await sendEmail()` instead of `waitUntil` | ~5 | +| `workers/index.ts` (`POST /emails`) | Fire-and-forget `email-sent` webhook after successful sync send | ~10 | +| `wrangler.jsonc` | Add `WEBHOOK_URL`, `WEBHOOK_SECRET` vars | ~3 | +| `workers/types.ts` | Add `WEBHOOK_URL`, `WEBHOOK_SECRET` to Env interface | ~2 | +| **No changes to `app.ts`** | CF Access Service Token validated at edge, existing JWT middleware handles it | 0 | + +**Total fork diff: ~82 lines across 5 files.** + +### Webhook Payload Contract + +**`email-received` webhook** (Agentic Inbox → Bridge): +```json +{ + "event": "email-received", + "mailboxId": "sofia.luz@ai.nimblersoft.com", + "emailId": "uuid-assigned-by-agentic-inbox", + "from": "sender@example.com", + "fromName": "Sender Name", + "to": ["sofia.luz@ai.nimblersoft.com"], + "cc": ["silas.vertiz@ai.nimblersoft.com"], + "subject": "Email subject", + "threadId": "thread-uuid", + "inReplyTo": "original-message-id", + "messageId": "original-message-id", + "hasAttachments": true, + "attachmentCount": 2, + "timestamp": "2026-06-13T01:05:12Z" +} +``` + +**`email-sent` webhook** (Agentic Inbox → Bridge, for dedup): +```json +{ + "event": "email-sent", + "mailboxId": "sofia.luz@ai.nimblersoft.com", + "emailId": "sent-email-uuid", + "threadId": "thread-uuid", + "to": "recipient@example.com", + "subject": "Re: Email subject", + "source": "web-ui", + "timestamp": "2026-06-13T01:06:30Z" +} +``` + +## Phases + +--- + +### Phase 0: Feature Review & Grilling Session ✅ COMPLETED + +**Goal:** Finalize bridge architecture decisions. + +**Status:** ✅ All decisions resolved during grilling session. See Decisions table above. + +**Decisions Locked:** +- [x] Approval: exact-match "approve" reply, zero polling +- [x] Corrections: MM outgoing webhook (no trigger word), bridge filters internally +- [x] Dedup: Agentic Inbox `email-sent` webhook → bridge marks resolved +- [x] Bridge deployment: Docker + CF Tunnel +- [x] Bridge stack: Hono + TypeScript +- [x] API auth: CF Access Service Token (no code changes) +- [x] Webhook auth: shared secrets +- [x] Webhook payload: reference only, bridge fetches body +- [x] Send: existing API + `?sync=true` +- [x] Signatures: Agentic Inbox settings, Hermes skips generation +- [x] Restart recovery: startup reconciliation +- [x] CC routing: dropped (Scenario A, independent delivery) +- [x] Built-in AI: disabled +- [x] Self-sent detection: kept + +--- + +### Phase 1: Setup & Deploy Agentic Inbox + +**Goal:** Deploy ericmaster/agentic-inbox at `ai.nimblersoft.com` on Cloudflare Workers with multi-domain support and bridge integration patches. + +**Steps:** +1.1 Clone `ericmaster/agentic-inbox` into `~/platform/agentic-inbox/` +1.2 Apply Commit 1: PR #49 patches (multi-domain support) +1.3 Apply Commit 2: Bridge integration patches (webhooks + sync send + auto-draft removal) +1.4 Initialize project `AGENTS.md` (setup context, deployment steps, fork modifications, known issues) +1.5 Configure `DOMAINS` var: `ai.nimblersoft.com,ericmaster.ninja,meliruns.com` + - `ai.nimblersoft.com` is the **only** inbox domain cut over in Phase 1 + - `ericmaster.ninja` and `meliruns.com` are listed in `DOMAINS` so the Worker recognizes them, but their MX/Email Routing cutover is **DEFERRED to Phase 4** — both are **live on Zoho** today (`mx.zoho.com`). Do **NOT** enable catch-all on them in Phase 1. + - `nimblersoft.com` is **NOT** included — stays on Google Workspace + - Add `ericmaster.com` later when Eric confirms +1.6 Set up Cloudflare resources: + - R2 bucket `agentic-inbox` + - Durable Objects binding + - Workers AI binding +1.7 Configure Cloudflare Access (one-click for Workers): + - Set `POLICY_AUD` and `TEAM_DOMAIN` secrets via Infisical (Project Name: `Agentic Inbox`, Project ID: `6c293d88-cc3c-4a44-8368-faa22c8c0196`) + - Create CF Access Service Token for bridge API access + - Create Access Policy allowing the Service Token + - Store Client ID + Client Secret in Infisical +1.8 Configure webhook secrets: + - Generate `WEBHOOK_SECRET` (shared with bridge) + - Set `WEBHOOK_URL` to `https://mail-bridge.nimblersoft.com/webhooks/agentic-inbox` + - Store both in Infisical +1.9 Set up DNS + Email Routing — **`ai.nimblersoft.com` ONLY in Phase 1**: + - `ai.nimblersoft.com` — point the Worker custom-domain record + enable CF Email Routing (catch-all → Agentic Inbox Worker). + - **Cutover, not dual-run:** `ai.nimblersoft.com` currently has a single MX → Mailgun (OpenMail's backend). Switching MX to CF Email Routing is a **hard cutover** — once flipped, OpenMail receives no new mail for this domain. This is NOT zero-disruption. Validate first against a throwaway test address/subdomain before flipping the live MX. + - **Rollback:** repoint MX back to Mailgun (OpenMail) if validation fails. + - **DNS coexistence check:** `ai.nimblersoft.com` must serve the Worker (HTTP) AND hold CF Email Routing MX simultaneously. CF supports this (MX + proxied A/AAAA coexist; the Worker route must NOT be a conflicting CNAME). **Verify** both the web UI loads and mail routes after the change. + - `nimblersoft.com` — **DO NOT change MX records. DO NOT set up Email Routing.** + - `ericmaster.ninja` — **DEFERRED to Phase 4.** Live on Zoho. Do NOT enable Email Routing/catch-all now (would hijack all inbound Zoho mail before the Zoho data migration). + - `meliruns.com` — **DEFERRED to Phase 4.** Live on Zoho. Same as above. + - **Verify:** `ai.nimblersoft.com` has Email Routing enabled + catch-all → Worker; the two Zoho domains remain untouched. +1.10 Enable Email Service send binding for `ai.nimblersoft.com` (requires domain verification + SPF/DKIM). Defer per-domain send setup for `ericmaster.ninja`/`meliruns.com` to Phase 4. +1.11 Deploy: `npm run deploy` +1.12 Create mailboxes: `sofia.luz@ai.nimblersoft.com`, `silas.vertiz@ai.nimblersoft.com` +1.13 Configure per-mailbox signatures in Agentic Inbox settings +1.14 Validate: send test email to each mailbox, verify receipt, test send, test web UI + +**Definition of Done:** +- [ ] Agentic Inbox deployed and accessible at `https://ai.nimblersoft.com` +- [ ] Multi-domain config active (DOMAINS lists `ai.nimblersoft.com` + the two deferred domains, but only `ai.nimblersoft.com` is cut over) +- [ ] Cloudflare Access configured (POLICY_AUD + TEAM_DOMAIN via Infisical) +- [ ] CF Access Service Token created and stored in Infisical +- [ ] Webhook secrets configured (WEBHOOK_URL + WEBHOOK_SECRET via Infisical) +- [ ] Email Routing catch-all rule active for `ai.nimblersoft.com` (and ONLY that domain) +- [ ] `ericmaster.ninja` + `meliruns.com` MX confirmed UNCHANGED (still Zoho) — cutover deferred to Phase 4 +- [ ] DNS coexistence verified on `ai.nimblersoft.com`: web UI loads AND inbound mail routes +- [ ] Email Service verified for outbound sending on `ai.nimblersoft.com` (SPF/DKIM) +- [ ] Test mailboxes created for sofia.luz@ai.nimblersoft.com and silas.vertiz@ai.nimblersoft.com +- [ ] Per-mailbox signatures configured +- [ ] Built-in AI agent disabled (no auto-draft on new email) +- [ ] Inbound: test email received, webhook fires, visible in web UI +- [ ] Outbound: test email sent (both sync and async modes) and received at external address +- [ ] Eric validates: sends and receives test emails personally +- [ ] `AGENTS.md` created in project root +- [ ] nimblersoft.com Google Workspace MX records confirmed UNMODIFIED + +**Risk Mitigation:** +- OpenMail stays operational for the OTHER OpenMail mailboxes during Phase 1, but `ai.nimblersoft.com` itself is a **hard MX cutover** (single MX). Validate on a test address/subdomain before flipping the live MX; rollback = repoint MX to Mailgun. +- If deployment fails: rollback DNS/Worker route + MX, keep OpenMail active +- Fork patches are small (~82 lines total) — easy to audit and revert +- Google Workspace MX on nimblersoft.com must NOT be touched +- `ericmaster.ninja` + `meliruns.com` stay on Zoho until Phase 4 — Phase 1 must not touch their MX/Email Routing + +--- + +### Phase 2: Bridge Migration + +**Goal:** Build webhook-driven bridge between Agentic Inbox and Mattermost. Integrate Hermes `/v1/responses` for persona-aware reply generation. Replace OpenMail dependencies. + +**Architecture:** +``` +┌─────────────────────┐ ┌──────────────────────────┐ ┌───────────────────┐ +│ Agentic Inbox │────>│ Bridge (Hono + TS) │────>│ Mattermost │ +│ Worker │ │ POST /webhooks/ │ │ (notifications) │ +│ ↓ (email-received │ │ agentic-inbox │ │ ↓ (outgoing │ +│ webhook) │ │ POST /webhooks/ │ │ webhook) │ +│ ↓ (email-sent │ │ mattermost │<────│ "approve" reply │ +│ webhook) │ │ ↓ │ │ correction reply │ +│ │ │ GET /api/v1/.../emails/:id│ └───────────────────┘ +│ │ │ (fetch email body) │ +│ │ │ ↓ │ +│ │ │ Hermes /v1/responses │ +│ │ │ ↓ │ +│ │ │ POST /api/v1/.../emails │────> Send email +│ │ │ (?sync=true) │ │ (via Agentic Inbox +│ │ │ │ │ API + CF Access +│ │ │ │ │ Service Token) +│ │ └──────────────────────────┘ +└─────────────────────┘ +``` + +**Steps:** + +2.1 **Build bridge project** (`~/agentic/agentic-inbox-bridge/`): + - Hono + TypeScript, Node.js runtime + - `Dockerfile` + `docker-compose.yml` (same pattern as `hermes-mm-bridge`) + - `AGENTS.md` with architecture, data flow, env vars + +2.2 **Bridge endpoints:** + - `POST /webhooks/agentic-inbox` — receives `email-received` and `email-sent` events from Agentic Inbox + - Validates `X-Webhook-Secret` header + - `email-received`: fetch email body via API → post MM notification → call Hermes → post suggestion as threaded reply → track in pending-replies + - `email-sent`: find pending by threadId → mark as "sent via web UI" → update MM post with audit trail + - `POST /webhooks/mattermost` — receives all posts from configured channel(s) via MM outgoing webhook + - Validates webhook token in payload + - If message is exact-match "approve" (case-insensitive, trimmed) AND is a reply to a known root_post_id → approval flow + - If message is a non-bot reply to a known root_post_id AND not "approve" → correction flow + - Otherwise → ignore + - `GET /health` — health check endpoint for Uptime Kuma monitoring + +2.3 **Bridge core logic:** + - **Agentic Inbox API client:** wraps all API calls with CF Access Service Token headers (`CF-Access-Client-Id` + `CF-Access-Client-Secret`) + - **Email fetch:** `GET /api/v1/mailboxes/:mailboxId/emails/:emailId` to retrieve full body + - **Email send:** `POST /api/v1/mailboxes/:mailboxId/emails?sync=true` with `{ to, from, subject, html, text, thread_id, in_reply_to }` + - **Hermes integration:** `POST /v1/responses` with persona system prompt from `config/agents.json` + - Conversation key: `agentic-inbox-{mailbox_address}-{thread_id}` + - Correction: `previous_response_id` chaining (same as current) + - System prompt updated: "No generes firma — se agrega automáticamente al enviar." + - **Per-agent routing:** `config.json` maps mailbox address → agent → MM channel + - **Pending replies:** `pending-replies.json` with reverse lookup (`root_post_id → suggestion_post_id`) + - **Self-sent detection:** if sender matches any configured mailbox address → notification only, no draft + - **Owner auto-send:** `AUTO_REPLY_OWNER_EMAILS` config — matching senders skip approval, send immediately + +2.4 **Mattermost outgoing webhook registration:** + - Register outgoing webhook on email channel(s) — **no trigger word** (fires on all posts) + - Webhook URL: `https://mail-bridge.nimblersoft.com/webhooks/mattermost` + - Content type: `application/json` + - Verify webhook fires reliably (test with manual post) + +2.5 **Configure CF Tunnel:** + - Add `mail-bridge.nimblersoft.com` route in `~/.cloudflared/config.yml` + - Point to bridge container port + - Restart cloudflared + +2.6 **Configure routing:** + - `config.json`: mailbox address → agent → channel mapping (same structure as current, keyed by address instead of OpenMail inbox ID) + - `config/agents.json`: agent personas with updated system prompts (no signature generation) + - `AUTO_REPLY_OWNER_EMAILS`: owner addresses that bypass approval + +2.7 **Test full flow:** + - Inbound: external email → Agentic Inbox → webhook → bridge fetches body → MM notification → Hermes draft → "approve" → send + - Correction: reply to suggestion thread → MM webhook → bridge → revise via Hermes → MM post update + - Owner auto-send: skip approval for configured sender + - Self-sent: Sofia sends to Silas → notification only, no draft + - Web UI dedup: approve via web UI → email-sent webhook → bridge marks resolved → MM post updated + - MM + web UI race: both triggered → dedup lock prevents duplicate send + +2.8 **Test restart recovery:** + - Stop bridge → type "approve" in MM → start bridge → verify reconciliation picks up missed approval + +2.9 **Test with both mailboxes:** sofia.luz@ai.nimblersoft.com and silas.vertiz@ai.nimblersoft.com + +2.10 **Set up Uptime Kuma monitoring** for `https://mail-bridge.nimblersoft.com/health` + +**Definition of Done:** +- [ ] Bridge running as Docker container behind CF Tunnel +- [ ] Full inbound flow working: email → webhook → fetch → MM → Hermes draft → "approve" → sent +- [ ] Correction flow working via MM outgoing webhook (no polling) +- [ ] Per-agent personas active (Sofia/Silas) with updated system prompts +- [ ] Web UI + MM "approve" deduplication working (no duplicate sends) +- [ ] Owner auto-send working +- [ ] Self-sent loop detection working +- [ ] Restart recovery working (missed "approve" reconciled on boot) +- [ ] Both mailboxes tested end-to-end +- [ ] Uptime Kuma monitoring active +- [ ] `AGENTS.md` created in bridge project root +- [ ] No OpenMail dependencies in new bridge code + +--- + +### Phase 3: Decommission OpenMail + +**Goal:** Remove all OpenMail references, stop bridge, clean up credentials. + +**Steps:** +3.1 Verify Phase 2 running for ≥7 days with no issues +3.2 Stop OpenMail bridge: `docker compose down` in `~/agentic/hermes-mail-mm-bridge/` +3.3 Remove OpenMail CLI: `npm uninstall -g @openmail/cli` +3.4 Remove `~/.openmail-cli/` config +3.5 Rotate/remove OpenMail API key from Infisical +3.6 Archive `~/agentic/hermes-mail-mm-bridge/` → `~/agentic/hermes-mail-mm-bridge.archived/` +3.7 Update project context in AGENTS.md across relevant projects +3.8 DNS cleanup: verify `ai.nimblersoft.com` points solely to Agentic Inbox Worker (remove any OpenMail-specific routes) +3.9 Update `nimbler-ops/AGENTS.md` project table: move `hermes-mail-mm-bridge` to archived, add `agentic-inbox-bridge` + +**Definition of Done:** +- [ ] OpenMail bridge stopped and disabled +- [ ] No running processes depending on OpenMail +- [ ] API key removed from Infisical and all configs +- [ ] CLI uninstalled +- [ ] Project archived (not deleted — reference) +- [ ] All docs updated +- [ ] DNS confirmed: only Agentic Inbox serves ai.nimblersoft.com +- [ ] `nimbler-ops/AGENTS.md` updated + +--- + +### Phase 4: Zoho Migration & Subscription Cancellation (DEFERRED) + +**Priority:** LOW — defer until Phases 1-3 complete and agentic-inbox stable for ≥2 weeks. + +**Goal:** Cut over `ericmaster.ninja` + `meliruns.com` inbound mail from Zoho to Agentic Inbox, migrate select Zoho emails, and cancel the Zoho subscription. **The MX/Email-Routing cutover for these two domains lives HERE, not in Phase 1** — flipping it earlier would hijack live Zoho inbound before the data is migrated. + +**Steps (scope to be refined when activated):** +4.1 Audit Zoho Mail: which accounts, how much data, which emails to migrate +4.2 Export from Zoho (Eric to check export options: IMAP, .eml, etc.) +4.3 Determine import mechanism to Agentic Inbox (API bulk upload? DO SQLite direct? R2 attachments?) +4.4 Execute partial migration (specific emails only, recent) +4.5 Verify imported emails visible and searchable +4.6 **Cut over MX:** enable CF Email Routing + catch-all → Worker on `ericmaster.ninja` and `meliruns.com`, replacing Zoho MX. Enable Email Service send (SPF/DKIM) per domain. Validate inbound + outbound per domain before declaring done. +4.7 Cancel Zoho subscription after verification period + +**Definition of Done:** +- [ ] Selected Zoho emails imported and accessible in Agentic Inbox +- [ ] Search functionality verified on imported emails +- [ ] `ericmaster.ninja` + `meliruns.com` MX cut over to CF Email Routing; inbound + outbound verified per domain +- [ ] Zoho subscription cancelled + +--- + +## Risk Register + +| Risk | Impact | Likelihood | Mitigation | +|------|--------|-----------|------------| +| PR #49 not compatible with latest upstream | Medium | Low | Fork is clean; patches are small and well-scoped | +| Multi-domain Email Routing misconfigured | High | Medium | Test each domain individually before cutover | +| Phase 1 catch-all hijacks live Zoho mail on ericmaster.ninja/meliruns.com | Critical | Medium | Both domains' MX/catch-all cutover deferred to Phase 4; Phase 1 touches `ai.nimblersoft.com` only; Phase 1 DoD asserts their MX unchanged | +| `ai.nimblersoft.com` MX cutover loses in-flight mail | Medium | Low | Hard single-MX cutover — validate on test address first; rollback = repoint MX to Mailgun | +| Bridge webhook delivery fails (bridge down) | Medium | Low | Email still stored in DO. Bridge reconciles on restart. Uptime Kuma alerts. | +| MM outgoing webhook misconfigured | Medium | Low | Test with manual post before go-live | +| Dual-approval (MM "approve" + web UI) causes duplicate sends | Medium | Low | `email-sent` webhook + dedup lock in pending-replies.json | +| CF Access Service Token expires/rotated | Medium | Low | Monitor token health. Stored in Infisical for easy rotation. | +| Hermes persona integration API changes | Low | Low | `/v1/responses` contract is stable; conversation keying unchanged | +| Email delivery disruption during cutover | High | Low | Phased approach; both providers active until Phase 3 | +| Zoho import not supported by Agentic Inbox | High | High | May need manual DO SQLite injection or direct import script — assess when Phase 4 activates | +| nimblersoft.com MX accidentally modified | Critical | Low | Explicit gate in Phase 1 DoD — verify Google Workspace MX unmodified | +| `?sync=true` send timeout (slow delivery) | Low | Low | Email Service delivery is typically <1s. Bridge has generous timeout. | + +## Files Likely to Change + +| File/Directory | Phase | Change Type | +|------|-------|-------------| +| `~/platform/agentic-inbox/` (NEW) | 1 | Created (clone of fork) | +| `~/platform/agentic-inbox/AGENTS.md` (NEW) | 1 | Created | +| `~/platform/agentic-inbox/workers/index.ts` | 1 | PR #49 patches + webhook emission + sync send + auto-draft removal | +| `~/platform/agentic-inbox/workers/types.ts` | 1 | Add WEBHOOK_URL, WEBHOOK_SECRET to Env | +| `~/platform/agentic-inbox/wrangler.jsonc` | 1 | PR #49 patches + webhook vars + multi-domain DOMAINS | +| `~/platform/agentic-inbox/README.md` | 1 | PR #49 patches | +| `~/agentic/agentic-inbox-bridge/` (NEW) | 2 | Created — new bridge project (Hono + TS) | +| `~/agentic/agentic-inbox-bridge/AGENTS.md` (NEW) | 2 | Created | +| `~/agentic/hermes-mail-mm-bridge/` | 3 | Archived | +| Cloudflare Email Routing rules | 1, 4 | Phase 1: `ai.nimblersoft.com` only · Phase 4: `ericmaster.ninja` + `meliruns.com` (Zoho cutover) | +| Cloudflare Access Service Token | 1 | Created for bridge API access | +| Cloudflare Worker secrets (Infisical) | 1 | POLICY_AUD, TEAM_DOMAIN, WEBHOOK_URL, WEBHOOK_SECRET | +| CF Tunnel config (`~/.cloudflared/config.yml`) | 2 | Add mail-bridge.nimblersoft.com route | +| MM outgoing webhook config | 2 | Created for email channel(s) | +| Uptime Kuma monitor | 2 | Add mail-bridge health check | +| `nimbler-ops/AGENTS.md` | 2, 3 | Updated project tables | + +## Delegation Strategy + +| Phase | Execution | Rationale | +|-------|-----------|-----------| +| **Phase 0** | ✅ **COMPLETED** | All decisions resolved during grilling session | +| **Phase 1** | Orchestrator → 2 subagents parallel | A: clone + patch + AGENTS.md / B: CF resources + secrets + Access | +| **Phase 2** | Orchestrator → subagent (impl) + subagent (review) | Bridge build + code review. Architecture fully pre-defined. | +| **Phase 3** | **Sequential orchestrator** | Safety-sensitive — no parallel ops on decommission | +| **Phase 4** | Delegated when activated | Low priority, deferred | + +## Phase Status Tracking + +| Phase | Status | Notes | +|-------|--------|-------| +| Phase 0 | ✅ Completed | Grilling session resolved all architecture decisions | +| Phase 1 | ⬜ Not Started | Plan approved, awaiting Phase 1 kickoff | +| Phase 2 | ⬜ Not Started | Requires Phase 1 completion. Architecture fully defined. | +| Phase 3 | ⬜ Not Started | Requires Phase 2 stable (≥7 days) | +| Phase 4 | ⬜ Deferred | Low priority. Activate after Phase 3. | From 8d8e173b94b64f621fe5db871d7a8efa8c13b037 Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Tue, 16 Jun 2026 20:58:04 +0000 Subject: [PATCH 4/9] config: set DOMAINS to nimblersoft multi-domain list (Phase 1 = ai.* only) ai.nimblersoft.com is the only domain cut over in Phase 1. ericmaster.ninja and meliruns.com are recognized but stay on Zoho (deferred to Phase 4). nimblersoft.com is intentionally excluded (Google Workspace). Co-Authored-By: Claude Opus 4.8 (1M context) --- wrangler.jsonc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wrangler.jsonc b/wrangler.jsonc index 7d9838c2..e3a7c94a 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -21,7 +21,10 @@ // DOMAINS accepts a single domain or a comma-separated list to serve multiple // domains from one instance, e.g. "example.com,another.com". Each domain needs its // own Email Routing catch-all rule, and must be verified for outbound sending. - "DOMAINS": "example.com", + // Phase 1 cuts over ai.nimblersoft.com ONLY. ericmaster.ninja + meliruns.com are + // listed for recognition but remain live on Zoho — their Email Routing/MX cutover is + // deferred to PLAN Phase 4. nimblersoft.com is intentionally absent (Google Workspace). + "DOMAINS": "ai.nimblersoft.com,ericmaster.ninja,meliruns.com", // EMAIL_ADDRESSES optionally restricts mailbox creation to specific addresses, and // may span the configured domains, e.g. ["hello@example.com", "hi@another.com"]. "EMAIL_ADDRESSES": [] From ee4a4875372542957d0799892d0e09f2b3d29792 Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Tue, 16 Jun 2026 21:52:22 +0000 Subject: [PATCH 5/9] config: serve web UI at ai.nimblersoft.com via Worker custom domain Proxied custom-domain record (behind Cloudflare Access). Coexists with the ai.nimblersoft.com MX; does not touch the nimblersoft.com apex. Co-Authored-By: Claude Opus 4.8 (1M context) --- wrangler.jsonc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wrangler.jsonc b/wrangler.jsonc index e3a7c94a..2552924e 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -6,6 +6,12 @@ "observability": { "enabled": true }, + // Web UI served at ai.nimblersoft.com (behind Cloudflare Access). Creates a PROXIED + // custom-domain record that coexists with the ai.nimblersoft.com MX (Email Routing). + // Does NOT touch the nimblersoft.com apex. + "routes": [ + { "pattern": "ai.nimblersoft.com", "custom_domain": true } + ], "compatibility_flags": [ "nodejs_compat" ], From a87ae58e3db1b07016590e34c431d2d85870c090 Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Tue, 16 Jun 2026 21:58:34 +0000 Subject: [PATCH 6/9] config: document ai.nimblersoft.com custom domain is API-managed @cloudflare/vite-plugin strips `routes` from the generated deploy config, so custom domains can't be declared in wrangler.jsonc for this stack. Replace the inert `routes` block with a comment pointing to the Workers Domains API call that actually attaches ai.nimblersoft.com -> agentic-inbox. Co-Authored-By: Claude Opus 4.8 (1M context) --- wrangler.jsonc | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/wrangler.jsonc b/wrangler.jsonc index 2552924e..844267eb 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -6,12 +6,14 @@ "observability": { "enabled": true }, - // Web UI served at ai.nimblersoft.com (behind Cloudflare Access). Creates a PROXIED - // custom-domain record that coexists with the ai.nimblersoft.com MX (Email Routing). - // Does NOT touch the nimblersoft.com apex. - "routes": [ - { "pattern": "ai.nimblersoft.com", "custom_domain": true } - ], + // Web UI is served at ai.nimblersoft.com via a Worker CUSTOM DOMAIN (behind Cloudflare + // Access). NOTE: @cloudflare/vite-plugin strips `routes` from the generated deploy config + // (build/server/wrangler.json), so custom domains here are NOT applied by `npm run deploy`. + // The custom domain is managed out-of-band via the Workers Domains API: + // PUT /accounts/{acct}/workers/domains {zone_id, hostname:"ai.nimblersoft.com", + // service:"agentic-inbox", environment:"production"} + // It creates a PROXIED AAAA (100::) that coexists with the ai.nimblersoft.com MX and does + // NOT touch the nimblersoft.com apex. "compatibility_flags": [ "nodejs_compat" ], From 6398c38be54fc70c680c56b820698d28cd165d4b Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Tue, 16 Jun 2026 23:25:52 +0000 Subject: [PATCH 7/9] docs: record Phase 1 live-infra status + inbound Email Routing blocker Steps A-F done (R2, DOMAINS, Access+service token, secrets, deploy, custom domain, Email Sending). Step 1.9 (inbound Email Routing) blocked: ai.nimblersoft.com is a subdomain in the nimblersoft.com/Google zone and the enable wizard forces apex MX + duplicate apex SPF. Stopped per guardrail. Documents resolution options (preferred: ai.nimblersoft.com as its own CF zone) + gotchas (vite-plugin strips routes; routing-enable not token-grantable). Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 20 ++++++++++++++++++ PLAN.md | 61 ++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index df398790..93c1d21d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,3 +82,23 @@ npm run deploy # wrangler deploy — DO NOT run unsupervised (live infra) - The bridge itself (`~/agentic/agentic-inbox-bridge/`) is built in PLAN Phase 2; this repo only emits webhooks to it. - Generated `worker-configuration.d.ts` and `build/` are gitignored. + +## Live deployment status (2026-06-16, Phase 1 supervised) + +**Deployed & live:** Worker `agentic-inbox` (bindings provisioned), R2 `agentic-inbox`, Access app +`Agentic Inbox`→`ai.nimblersoft.com` (`POLICY_AUD=6189a26a…`, team `nimblersoft.cloudflareaccess.com`), +service token `agentic-inbox-bridge`, custom domain `ai.nimblersoft.com` (proxied AAAA, web UI behind +Access — 302→Access confirmed), Email **Sending** onboarded (`cf-bounce.*` DKIM/SPF/DMARC; no SPF +collision). Secrets in Infisical **Agentic Inbox/prod**: `POLICY_AUD`, `TEAM_DOMAIN`, `WEBHOOK_URL`, +`WEBHOOK_SECRET`, `CF_ACCESS_CLIENT_ID`, `CF_ACCESS_CLIENT_SECRET`. + +**Blocked:** inbound **Email Routing** (PLAN 1.9). `ai.nimblersoft.com` is a subdomain inside the +`nimblersoft.com` (Google) zone; the Email Routing enable wizard is zone-level and forces apex MX + +a duplicate apex SPF → would endanger Google Workspace. Stopped per guardrail. See PLAN.md "Execution +Notes — Live Infra" for the resolution options (preferred: make `ai.nimblersoft.com` its own CF zone). + +**Gotchas learned:** (1) `@cloudflare/vite-plugin` strips `routes` from the generated deploy config → +custom domains are managed via the Workers Domains API, not `wrangler.jsonc`. (2) Enabling Email +Routing is **not** an API-token permission (only "Email Routing Rules" is) — it needs the dashboard or +the `email_routing:write` OAuth scope. (3) Scoped `CLOUDFLARE_EMAIL_API_TOKEN` lives in Infisical +**Nimblerbox/dev** (DNS + Email Routing Rules + Email Sending + Email Routing Addresses : Edit). diff --git a/PLAN.md b/PLAN.md index 5dcb5418..74d2f39e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -187,24 +187,24 @@ Two commits on the fork: 1.13 Configure per-mailbox signatures in Agentic Inbox settings 1.14 Validate: send test email to each mailbox, verify receipt, test send, test web UI -**Definition of Done:** -- [ ] Agentic Inbox deployed and accessible at `https://ai.nimblersoft.com` -- [ ] Multi-domain config active (DOMAINS lists `ai.nimblersoft.com` + the two deferred domains, but only `ai.nimblersoft.com` is cut over) -- [ ] Cloudflare Access configured (POLICY_AUD + TEAM_DOMAIN via Infisical) -- [ ] CF Access Service Token created and stored in Infisical -- [ ] Webhook secrets configured (WEBHOOK_URL + WEBHOOK_SECRET via Infisical) -- [ ] Email Routing catch-all rule active for `ai.nimblersoft.com` (and ONLY that domain) -- [ ] `ericmaster.ninja` + `meliruns.com` MX confirmed UNCHANGED (still Zoho) — cutover deferred to Phase 4 -- [ ] DNS coexistence verified on `ai.nimblersoft.com`: web UI loads AND inbound mail routes -- [ ] Email Service verified for outbound sending on `ai.nimblersoft.com` (SPF/DKIM) -- [ ] Test mailboxes created for sofia.luz@ai.nimblersoft.com and silas.vertiz@ai.nimblersoft.com -- [ ] Per-mailbox signatures configured -- [ ] Built-in AI agent disabled (no auto-draft on new email) -- [ ] Inbound: test email received, webhook fires, visible in web UI -- [ ] Outbound: test email sent (both sync and async modes) and received at external address -- [ ] Eric validates: sends and receives test emails personally -- [ ] `AGENTS.md` created in project root -- [ ] nimblersoft.com Google Workspace MX records confirmed UNMODIFIED +**Definition of Done:** (status as of 2026-06-16 — see Execution Notes above) +- [x] Agentic Inbox deployed and accessible at `https://ai.nimblersoft.com` (HTTP/web UI behind Access; 302→Access login confirmed) +- [x] Multi-domain config active (DOMAINS lists `ai.nimblersoft.com` + the two deferred domains, but only `ai.nimblersoft.com` is cut over) +- [x] Cloudflare Access configured (POLICY_AUD + TEAM_DOMAIN via Infisical) +- [x] CF Access Service Token created and stored in Infisical (verified 200 against live API) +- [x] Webhook secrets configured (WEBHOOK_URL + WEBHOOK_SECRET via Infisical + Worker) +- [ ] ~~Email Routing catch-all rule~~ → **BLOCKED** (apex-MX wizard). Plan revised to literal per-mailbox rules; needs zone-strategy decision first. +- [x] `ericmaster.ninja` + `meliruns.com` MX confirmed UNCHANGED (still Zoho) — cutover deferred to Phase 4 +- [~] DNS coexistence on `ai.nimblersoft.com`: **web UI loads ✅** (proxied AAAA + MX coexist); **inbound routing ⛔ blocked** +- [x] Email Service verified for outbound sending on `ai.nimblersoft.com` (DKIM/SPF/DMARC via `cf-bounce.*`; status syncing→verified) +- [ ] Test mailboxes created for sofia.luz@ / silas.vertiz@ai.nimblersoft.com — pending Step G +- [ ] Per-mailbox signatures configured — pending Step G +- [x] Built-in AI agent disabled (fork commit 47ef682 removes the auto-draft trigger) +- [ ] Inbound: test email received, webhook fires, visible in web UI — **blocked on Step G** +- [ ] Outbound: test email sent (both sync and async modes) and received externally — pending Step G +- [ ] Eric validates: sends and receives test emails personally — pending Step G +- [x] `AGENTS.md` created in project root +- [x] nimblersoft.com Google Workspace MX records confirmed UNMODIFIED (apex MX = aspmx.l.google.com, 1 SPF record, zero Cloudflare at apex) **Risk Mitigation:** - OpenMail stays operational for the OTHER OpenMail mailboxes during Phase 1, but `ai.nimblersoft.com` itself is a **hard MX cutover** (single MX). Validate on a test address/subdomain before flipping the live MX; rollback = repoint MX to Mailgun. @@ -213,6 +213,29 @@ Two commits on the fork: - Google Workspace MX on nimblersoft.com must NOT be touched - `ericmaster.ninja` + `meliruns.com` stay on Zoho until Phase 4 — Phase 1 must not touch their MX/Email Routing +**Execution Notes — Live Infra (2026-06-16, supervised):** + +Steps **1.5–1.10 + 1.11 done; 1.9 (inbound Email Routing) BLOCKED; 1.12–1.14 pending.** + +Done & verified: +- **R2** bucket `agentic-inbox` created. **Worker deployed** (`agentic-inbox`, all bindings: MAILBOX/EMAIL_AGENT/EMAIL_MCP DOs, EMAIL send, BUCKET, AI). `DOMAINS=ai.nimblersoft.com,ericmaster.ninja,meliruns.com`. +- **Cloudflare Access:** self-hosted app `Agentic Inbox` → `ai.nimblersoft.com`, `POLICY_AUD=6189a26a…`, `TEAM_DOMAIN=https://nimblersoft.cloudflareaccess.com`. Service token `agentic-inbox-bridge` + 2 policies (Eric email; bridge service-token non_identity). Service token verified 200 against live `/api/v1/config`. +- **Secrets** (Worker `wrangler secret put` + mirrored to Infisical **Agentic Inbox/prod**): `POLICY_AUD`, `TEAM_DOMAIN`, `WEBHOOK_URL=https://mail-bridge.nimblersoft.com/webhooks/agentic-inbox`, `WEBHOOK_SECRET` (random). Bridge service-token creds in Infisical as `CF_ACCESS_CLIENT_ID`/`CF_ACCESS_CLIENT_SECRET`. +- **Custom domain:** `ai.nimblersoft.com` → Worker via Workers Domains API (proxied AAAA `100::`), coexists with MX. NOTE: `@cloudflare/vite-plugin` strips `routes` from the generated deploy config, so custom domains are API-managed, not via `wrangler.jsonc`. +- **Email Sending (outbound):** `ai.nimblersoft.com` onboarded (dashboard). CF isolated auth under `cf-bounce.ai.nimblersoft.com` (own SPF→cloudflare, MX→route*.mx.cloudflare.net for bounces) + DKIM `cf-bounce._domainkey.ai.nimblersoft.com` + `_dmarc.ai.nimblersoft.com p=reject`. **No SPF collision** — the `ai.nimblersoft.com` SPF (mailgun) was left untouched. +- **Credentials:** scoped `CLOUDFLARE_EMAIL_API_TOKEN` in Infisical **Nimblerbox/dev** (DNS:Edit + Email Routing Rules:Edit + Email Sending:Edit + Email Routing Addresses:Edit, all zones + account). The wrangler OAuth token lacks `email_*:write`; the broad `CLOUDFLARE_API_TOKEN` lacks email perms. + +DNS before/after on `ai.nimblersoft.com`: MX unchanged (mailgun, prio 10); **added** proxied AAAA `100::` (web UI) + `cf-bounce.*` sending records. Apex `nimblersoft.com` MX = `aspmx.l.google.com` **UNCHANGED**. Zoho domains UNCHANGED. + +**BLOCKER — Step 1.9 (inbound Email Routing):** `ai.nimblersoft.com` is a **subdomain inside the `nimblersoft.com` zone** (no separate zone). Cloudflare's Email Routing **enable wizard is zone-level and forces apex records** — it wanted to add `nimblersoft.com` MX→`route1/2/3.mx.cloudflare.net` (a mail-loss fallback trap behind Google's prio-0 MX) **and a duplicate apex SPF** (`v=spf1 include:_spf.mx.cloudflare.net` → two SPF records = permerror, degrades Google Workspace SPF). Enabling routing is **not exposed as an API-token permission** (only "Email Routing Rules" exists, which manages rules but not the enable toggle), so it can't be done apex-safely via the token. **Stopped per guardrail 1** (never touch `nimblersoft.com` apex MX). + +**Recommended resolution (decide before resuming G):** +1. **Preferred — make `ai.nimblersoft.com` its own Cloudflare zone** (subdomain zone via NS delegation from `nimblersoft.com`). Then Email Routing operates on `ai.nimblersoft.com`'s *own* apex — MX live on `ai.nimblersoft.com`, fully isolated from `nimblersoft.com`/Google. Migration cost: re-create the Worker custom domain + Email Sending onboarding in the new zone. +2. **Quick — enable via wizard, then surgically delete the CF apex MX + dup SPF + DKIM** (restores apex to Google baseline; routing-enabled flag persists; ~seconds window, low risk given Google prio-0 + DMARC p=none). Requires explicit apex-modification authorization. +3. **API — `wrangler login` to grant `email_routing:write`, then enable with `skip_wizard`** (may avoid apex DNS entirely; headless OAuth-callback friction). + +After G is unblocked: literal per-mailbox routing rules (`sofia.luz@`/`silas.vertiz@ → Worker`) instead of a zone-wide catch-all (avoids intercepting the `cf-bounce` MX); inbound handler has **no domain guard** — it stores only for addresses with an existing mailbox. Cutover = delete `ai.nimblersoft.com` mailgun MX (Eric authorized); rollback = re-add `mxa/mxb.eu.mailgun.org` prio 10. + --- ### Phase 2: Bridge Migration @@ -424,7 +447,7 @@ Two commits on the fork: | Phase | Status | Notes | |-------|--------|-------| | Phase 0 | ✅ Completed | Grilling session resolved all architecture decisions | -| Phase 1 | ⬜ Not Started | Plan approved, awaiting Phase 1 kickoff | +| Phase 1 | 🟡 In Progress | Live infra A–F done (R2, DOMAINS, Access+service token, secrets, deploy, custom domain, Email Sending). **Step 1.9 inbound Email Routing BLOCKED** (apex-MX wizard — see Phase 1 Execution Notes); mailboxes/validation pending on it. | | Phase 2 | ⬜ Not Started | Requires Phase 1 completion. Architecture fully defined. | | Phase 3 | ⬜ Not Started | Requires Phase 2 stable (≥7 days) | | Phase 4 | ⬜ Deferred | Low priority. Activate after Phase 3. | From 3b1e54c0d5179c1ebe1d16c39bc710f679e91c90 Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Wed, 17 Jun 2026 00:10:41 +0000 Subject: [PATCH 8/9] docs: shared-zone Email Routing ruled out; pivot to dedicated domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enabling Email Routing on nimblersoft.com errors: "Existing non-Cloudflare MX records conflict with Email Routing" — CF refuses while the apex has Google MX, and there is no subdomain-only path on a shared zone. Decision: dedicated domain (its own CF zone, mail at apex). Worker/R2/DOs/bridge contracts carry over. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 11 +++++++---- PLAN.md | 20 ++++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 93c1d21d..693a4c7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,10 +92,13 @@ Access — 302→Access confirmed), Email **Sending** onboarded (`cf-bounce.*` D collision). Secrets in Infisical **Agentic Inbox/prod**: `POLICY_AUD`, `TEAM_DOMAIN`, `WEBHOOK_URL`, `WEBHOOK_SECRET`, `CF_ACCESS_CLIENT_ID`, `CF_ACCESS_CLIENT_SECRET`. -**Blocked:** inbound **Email Routing** (PLAN 1.9). `ai.nimblersoft.com` is a subdomain inside the -`nimblersoft.com` (Google) zone; the Email Routing enable wizard is zone-level and forces apex MX + -a duplicate apex SPF → would endanger Google Workspace. Stopped per guardrail. See PLAN.md "Execution -Notes — Live Infra" for the resolution options (preferred: make `ai.nimblersoft.com` its own CF zone). +**Blocked → re-platforming to a dedicated domain.** Inbound **Email Routing** (PLAN 1.9) is impossible +on the shared `nimblersoft.com`/Google zone: enabling Email Routing errors with *"Existing +non-Cloudflare MX records conflict with Email Routing"* — CF refuses while the apex has Google MX, and +there's no subdomain-only path. **Decision: use a dedicated domain** (its own CF zone, mail at the +apex). Eric is acquiring one. The `ai.nimblersoft.com` Access app / custom domain / Email Sending +onboarding become moot once it lands (most other work — Worker, R2, DOs, bridge contracts — carries +over). See PLAN.md "Execution Notes — Live Infra". **Gotchas learned:** (1) `@cloudflare/vite-plugin` strips `routes` from the generated deploy config → custom domains are managed via the Workers Domains API, not `wrangler.jsonc`. (2) Enabling Email diff --git a/PLAN.md b/PLAN.md index 74d2f39e..fbd4b6f7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -229,10 +229,22 @@ DNS before/after on `ai.nimblersoft.com`: MX unchanged (mailgun, prio 10); **add **BLOCKER — Step 1.9 (inbound Email Routing):** `ai.nimblersoft.com` is a **subdomain inside the `nimblersoft.com` zone** (no separate zone). Cloudflare's Email Routing **enable wizard is zone-level and forces apex records** — it wanted to add `nimblersoft.com` MX→`route1/2/3.mx.cloudflare.net` (a mail-loss fallback trap behind Google's prio-0 MX) **and a duplicate apex SPF** (`v=spf1 include:_spf.mx.cloudflare.net` → two SPF records = permerror, degrades Google Workspace SPF). Enabling routing is **not exposed as an API-token permission** (only "Email Routing Rules" exists, which manages rules but not the enable toggle), so it can't be done apex-safely via the token. **Stopped per guardrail 1** (never touch `nimblersoft.com` apex MX). -**Recommended resolution (decide before resuming G):** -1. **Preferred — make `ai.nimblersoft.com` its own Cloudflare zone** (subdomain zone via NS delegation from `nimblersoft.com`). Then Email Routing operates on `ai.nimblersoft.com`'s *own* apex — MX live on `ai.nimblersoft.com`, fully isolated from `nimblersoft.com`/Google. Migration cost: re-create the Worker custom domain + Email Sending onboarding in the new zone. -2. **Quick — enable via wizard, then surgically delete the CF apex MX + dup SPF + DKIM** (restores apex to Google baseline; routing-enabled flag persists; ~seconds window, low risk given Google prio-0 + DMARC p=none). Requires explicit apex-modification authorization. -3. **API — `wrangler login` to grant `email_routing:write`, then enable with `skip_wizard`** (may avoid apex DNS entirely; headless OAuth-callback friction). +**CONCLUSIVE (2026-06-17):** attempting to enable Email Routing on `nimblersoft.com` returns +*"Existing non-Cloudflare MX records conflict with Email Routing. Remove or update them and try +again."* — Cloudflare **refuses to enable Email Routing while the apex has foreign (Google) MX** and +demands their removal. There is **no subdomain-only path on a shared zone**: routing is apex-gated and +incompatible with keeping Google on the apex. The shared-`nimblersoft.com`-zone approach is therefore +**ruled out** (the apex enable also errored cleanly — added nothing; apex verified pristine, 68 records). + +**DECISION:** use a **dedicated domain** for the agent inbox (its own Cloudflare zone, mail at the +apex — the single-domain setup agentic-inbox is designed for). Eric is acquiring a new domain and +adding it to Cloudflare. This also gives reputation/blast-radius isolation from Google Workspace. + +When the dedicated domain's zone is active in CF, resume Phase 1 for it (most work carries over — +Worker code/R2/DOs/AI/bridge contracts unchanged; replay DOMAINS, Access app+POLICY_AUD, custom +domain, Email Sending onboarding, then Email Routing on the apex + literal mailbox rules). The +`ai.nimblersoft.com` Access app / custom domain / Email Sending onboarding become moot (decommission +or repurpose the web-UI host as a follow-up). After G is unblocked: literal per-mailbox routing rules (`sofia.luz@`/`silas.vertiz@ → Worker`) instead of a zone-wide catch-all (avoids intercepting the `cf-bounce` MX); inbound handler has **no domain guard** — it stores only for addresses with an existing mailbox. Cutover = delete `ai.nimblersoft.com` mailgun MX (Eric authorized); rollback = re-add `mxa/mxb.eu.mailgun.org` prio 10. From 1a08b79f902384fe7d0b3f726bff8a4246c09f09 Mon Sep 17 00:00:00 2001 From: Eric Aguayo Date: Wed, 17 Jun 2026 01:15:01 +0000 Subject: [PATCH 9/9] feat: cut Phase 1 over to dedicated domain nimblerbot.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared ai.nimblersoft.com/Google zone was ruled out — Cloudflare refuses to enable Email Routing while the apex has non-Cloudflare (Google) MX. Phase 1 is now live and validated on the dedicated zone nimblerbot.com: - DOMAINS=nimblerbot.com (was ai.nimblersoft.com,ericmaster.ninja,meliruns.com) - Web UI at ainbox.nimblerbot.com (proxied AAAA); mail at the apex - Email Routing (literal per-mailbox rules) + Email Sending live - Mailboxes sofia.luz@/silas.vertiz@nimblerbot.com; inbound + outbound validated internally and externally; global DNS propagation confirmed - PLAN.md / AGENTS.md updated from "blocked" to "complete" Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 55 ++++++++++++++++++++++++++++---------------------- PLAN.md | 48 +++++++++++++++++++++++++++---------------- wrangler.jsonc | 19 ++++++++--------- 3 files changed, 72 insertions(+), 50 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 693a4c7e..308e4097 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,9 @@ Working context for any agent operating on this project. Company-wide context li Self-hosted email for AI agents, replacing OpenMail. Fork of [`cloudflare/agentic-inbox`](https://github.com/cloudflare/agentic-inbox) running on Cloudflare Workers + Durable Objects (SQLite per mailbox) + R2 (attachments) + Email Routing + Email Service. -Deployed at `https://ai.nimblersoft.com`. +Deployed on the dedicated domain **`nimblerbot.com`** — web UI at `https://ainbox.nimblerbot.com` +(behind Access), mailboxes at `*@nimblerbot.com`. (The earlier `ai.nimblersoft.com` target was +abandoned — see PLAN.md; leftover CF resources await decommission.) - **Repo:** `ericmaster/agentic-inbox`, branch `feat/nimblersoft-multidomain-bridge`. - **Upstream base:** clean fork (last upstream commit `48039bb`, Merge PR #7). @@ -51,7 +53,7 @@ committed to `wrangler.jsonc`): | Key | Where | Notes | |---|---|---| -| `DOMAINS` | `wrangler.jsonc` var | `ai.nimblersoft.com,ericmaster.ninja,meliruns.com` — but only `ai.nimblersoft.com` is cut over in Phase 1 (the other two are live on Zoho; deferred to PLAN Phase 4). | +| `DOMAINS` | `wrangler.jsonc` var | `nimblerbot.com` (Phase 1 live). `ericmaster.ninja` + `meliruns.com` are live on Zoho — deferred to PLAN Phase 4. | | `EMAIL_ADDRESSES` | `wrangler.jsonc` var | optional allow-list of creatable mailbox addresses. | | `POLICY_AUD`, `TEAM_DOMAIN` | secret/var (Infisical) | Cloudflare Access. | | `WEBHOOK_URL` | var (Infisical) | `https://mail-bridge.nimblersoft.com/webhooks/agentic-inbox`. | @@ -74,8 +76,9 @@ npm run deploy # wrangler deploy — DO NOT run unsupervised (live infra) - **Live infra steps are human-gated.** Deploy, DNS/MX cutover, Email Routing, Email Service verification, and Infisical secret writes are NOT done autonomously. See PLAN.md Phase 1. -- `ai.nimblersoft.com` is a **single-MX hard cutover** (currently Mailgun/OpenMail). Validate on a - test address before flipping; rollback = repoint MX to Mailgun. +- Inbound runs on the dedicated zone `nimblerbot.com` (mail at the apex). The shared + `ai.nimblersoft.com`/Google zone was abandoned — CF refuses Email Routing while the apex has + non-Cloudflare (Google) MX. See PLAN.md. - **Do not** enable Email Routing/catch-all on `ericmaster.ninja` or `meliruns.com` before PLAN Phase 4 — both are live on Zoho and would have their inbound hijacked. - `nimblersoft.com` MX (Google Workspace) must never be touched. @@ -83,25 +86,29 @@ npm run deploy # wrangler deploy — DO NOT run unsupervised (live infra) emits webhooks to it. - Generated `worker-configuration.d.ts` and `build/` are gitignored. -## Live deployment status (2026-06-16, Phase 1 supervised) - -**Deployed & live:** Worker `agentic-inbox` (bindings provisioned), R2 `agentic-inbox`, Access app -`Agentic Inbox`→`ai.nimblersoft.com` (`POLICY_AUD=6189a26a…`, team `nimblersoft.cloudflareaccess.com`), -service token `agentic-inbox-bridge`, custom domain `ai.nimblersoft.com` (proxied AAAA, web UI behind -Access — 302→Access confirmed), Email **Sending** onboarded (`cf-bounce.*` DKIM/SPF/DMARC; no SPF -collision). Secrets in Infisical **Agentic Inbox/prod**: `POLICY_AUD`, `TEAM_DOMAIN`, `WEBHOOK_URL`, -`WEBHOOK_SECRET`, `CF_ACCESS_CLIENT_ID`, `CF_ACCESS_CLIENT_SECRET`. - -**Blocked → re-platforming to a dedicated domain.** Inbound **Email Routing** (PLAN 1.9) is impossible -on the shared `nimblersoft.com`/Google zone: enabling Email Routing errors with *"Existing -non-Cloudflare MX records conflict with Email Routing"* — CF refuses while the apex has Google MX, and -there's no subdomain-only path. **Decision: use a dedicated domain** (its own CF zone, mail at the -apex). Eric is acquiring one. The `ai.nimblersoft.com` Access app / custom domain / Email Sending -onboarding become moot once it lands (most other work — Worker, R2, DOs, bridge contracts — carries -over). See PLAN.md "Execution Notes — Live Infra". +## Live deployment status (2026-06-17 — Phase 1 COMPLETE on `nimblerbot.com`) + +**Live & validated** on the dedicated domain **`nimblerbot.com`** (its own CF zone, account +`71f942c5…` — the **same** account as the Worker, required for custom domain / routing-to-Worker / +Email Sending): +- Worker `agentic-inbox` (`DOMAINS=nimblerbot.com`); R2 `agentic-inbox`; DOs MAILBOX/EMAIL_AGENT/EMAIL_MCP; AI; EMAIL send. +- **Access** app `Agentic Inbox (nimblerbot.com)` → `ainbox.nimblerbot.com` (`id=efb1a4c3…`, `POLICY_AUD=814e4b55…`, team `nimblersoft.cloudflareaccess.com`); service token `agentic-inbox-bridge` (`658c03a3…`) in its policy. Service-token API → 200 confirmed. +- **Custom domain** `ainbox.nimblerbot.com` (proxied AAAA) — apex reserved for mail only. +- **Email Routing** on the apex; **literal** rules `sofia.luz@`/`silas.vertiz@ → Worker`; catch-all disabled (drop). +- **Email Sending** onboarded. Apex DNS (12 recs): 3× MX `route*.mx.cloudflare.net`, single SPF `v=spf1 include:_spf.mx.cloudflare.net ~all`, `_dmarc p=reject`, DKIM `cf2024-1` + `cf-bounce.*` (MX/SPF/DKIM). +- **Mailboxes:** `sofia.luz@nimblerbot.com`, `silas.vertiz@nimblerbot.com` (`mailboxId = lowercase address`). +- **Secrets** (Worker + Infisical **Agentic Inbox/prod**): `POLICY_AUD` (now `814e4b55…`), `TEAM_DOMAIN`, `WEBHOOK_URL`, `WEBHOOK_SECRET`, `CF_ACCESS_CLIENT_ID`, `CF_ACCESS_CLIENT_SECRET`. +- **Validated:** internal `sofia→silas`; external out `sofia→eric@nimblersoft.com` (landed in **inbox**, passes `p=reject`); external in `eric→sofia` (stored); global NS/MX/SPF/DKIM/DMARC propagation confirmed. + +**Abandoned — shared `ai.nimblersoft.com` zone:** CF refuses Email Routing while the `nimblersoft.com` +apex has Google MX (*"Existing non-Cloudflare MX records conflict…"*). The `ai.nimblersoft.com` Access +app (`142c5fd6…`) / custom domain / Email Sending onboarding are **moot and await decommission**. +`nimblersoft.com` (Google) and the Zoho domains were never touched. **Gotchas learned:** (1) `@cloudflare/vite-plugin` strips `routes` from the generated deploy config → -custom domains are managed via the Workers Domains API, not `wrangler.jsonc`. (2) Enabling Email -Routing is **not** an API-token permission (only "Email Routing Rules" is) — it needs the dashboard or -the `email_routing:write` OAuth scope. (3) Scoped `CLOUDFLARE_EMAIL_API_TOKEN` lives in Infisical -**Nimblerbox/dev** (DNS + Email Routing Rules + Email Sending + Email Routing Addresses : Edit). +custom domains via the Workers Domains API (wrangler OAuth token), not `wrangler.jsonc`. (2) Enabling +Email Routing / onboarding Email Sending is **not** an API-token permission (only "Email Routing Rules" +is) — done in the dashboard. (3) Zone + Worker must share a CF account. (4) `mailboxId = lowercase +email address`. (5) Scoped `CLOUDFLARE_EMAIL_API_TOKEN` in Infisical **Nimblerbox/dev** (DNS + Email +Routing Rules + Email Sending + Email Routing Addresses : Edit) manages rules; the wrangler OAuth token ++ broad `CLOUDFLARE_API_TOKEN` lack email-write scopes. diff --git a/PLAN.md b/PLAN.md index fbd4b6f7..1b73d476 100644 --- a/PLAN.md +++ b/PLAN.md @@ -187,24 +187,24 @@ Two commits on the fork: 1.13 Configure per-mailbox signatures in Agentic Inbox settings 1.14 Validate: send test email to each mailbox, verify receipt, test send, test web UI -**Definition of Done:** (status as of 2026-06-16 — see Execution Notes above) -- [x] Agentic Inbox deployed and accessible at `https://ai.nimblersoft.com` (HTTP/web UI behind Access; 302→Access login confirmed) -- [x] Multi-domain config active (DOMAINS lists `ai.nimblersoft.com` + the two deferred domains, but only `ai.nimblersoft.com` is cut over) -- [x] Cloudflare Access configured (POLICY_AUD + TEAM_DOMAIN via Infisical) -- [x] CF Access Service Token created and stored in Infisical (verified 200 against live API) -- [x] Webhook secrets configured (WEBHOOK_URL + WEBHOOK_SECRET via Infisical + Worker) -- [ ] ~~Email Routing catch-all rule~~ → **BLOCKED** (apex-MX wizard). Plan revised to literal per-mailbox rules; needs zone-strategy decision first. -- [x] `ericmaster.ninja` + `meliruns.com` MX confirmed UNCHANGED (still Zoho) — cutover deferred to Phase 4 -- [~] DNS coexistence on `ai.nimblersoft.com`: **web UI loads ✅** (proxied AAAA + MX coexist); **inbound routing ⛔ blocked** -- [x] Email Service verified for outbound sending on `ai.nimblersoft.com` (DKIM/SPF/DMARC via `cf-bounce.*`; status syncing→verified) -- [ ] Test mailboxes created for sofia.luz@ / silas.vertiz@ai.nimblersoft.com — pending Step G -- [ ] Per-mailbox signatures configured — pending Step G +**Definition of Done:** ✅ **COMPLETE on the dedicated domain `nimblerbot.com`** (2026-06-17). The original `ai.nimblersoft.com` target was abandoned (shared-zone apex-MX conflict — see Execution Notes); items below reflect the `nimblerbot.com` deployment. +- [x] Agentic Inbox deployed & accessible — web UI at `https://ainbox.nimblerbot.com` behind Access (service-token API returns 200; `GET /api/v1/config` → `{"domains":["nimblerbot.com"]}`) +- [x] Single-domain config active (`DOMAINS=nimblerbot.com`; the two Zoho domains dropped from the list — still deferred to Phase 4) +- [x] Cloudflare Access configured (new `POLICY_AUD=814e4b55…` + unchanged `TEAM_DOMAIN` via Infisical) +- [x] CF Access Service Token (`agentic-inbox-bridge`) reused in the new app's policies (verified 200 against live API) +- [x] Webhook secrets configured (WEBHOOK_URL + WEBHOOK_SECRET — carried over unchanged) +- [x] Email Routing enabled on `nimblerbot.com` apex with **literal per-mailbox rules** (`sofia.luz@`/`silas.vertiz@ → Worker`); catch-all left disabled/drop +- [x] `nimblersoft.com` (Google) + `ericmaster.ninja` + `meliruns.com` (Zoho) MX UNTOUCHED — different zones, nothing modified +- [x] DNS coexistence: web UI on `ainbox.*` (proxied AAAA) + mail on the apex (MX) — clean separation +- [x] Email Sending verified for outbound on `nimblerbot.com` (DKIM `cf2024-1` + `cf-bounce.*` SPF/DKIM + `_dmarc p=reject`; single clean apex SPF) +- [x] Mailboxes created: `sofia.luz@nimblerbot.com`, `silas.vertiz@nimblerbot.com` +- [ ] Per-mailbox signatures configured — optional, pending (offered to Eric) - [x] Built-in AI agent disabled (fork commit 47ef682 removes the auto-draft trigger) -- [ ] Inbound: test email received, webhook fires, visible in web UI — **blocked on Step G** -- [ ] Outbound: test email sent (both sync and async modes) and received externally — pending Step G -- [ ] Eric validates: sends and receives test emails personally — pending Step G +- [x] Inbound: external→Worker test received & stored (`eric→sofia.luz@`; CF-internal `sofia→silas` also stored) +- [x] Outbound: sync send tested (`sofia→silas` internal + `sofia→eric@nimblersoft.com` external — landed in inbox, passes `p=reject`) +- [x] Eric validated personally: received the outbound test in his inbox, sent an inbound that was confirmed stored - [x] `AGENTS.md` created in project root -- [x] nimblersoft.com Google Workspace MX records confirmed UNMODIFIED (apex MX = aspmx.l.google.com, 1 SPF record, zero Cloudflare at apex) +- [x] nimblersoft.com Google Workspace MX confirmed UNMODIFIED (separate zone entirely; never touched) **Risk Mitigation:** - OpenMail stays operational for the OTHER OpenMail mailboxes during Phase 1, but `ai.nimblersoft.com` itself is a **hard MX cutover** (single MX). Validate on a test address/subdomain before flipping the live MX; rollback = repoint MX to Mailgun. @@ -250,6 +250,20 @@ After G is unblocked: literal per-mailbox routing rules (`sofia.luz@`/`silas.ver --- +**✅ RESOLVED — live on `nimblerbot.com` (2026-06-17, supervised):** Eric acquired `nimblerbot.com` (registrar name.com → Cloudflare nameservers; zone active in account `71f942c5…`, the **same** account as the Worker — required for custom domain / routing-to-Worker / Email Sending). Phase 1 was replayed on it; the address↔mailbox mapping is `mailboxId = lowercase email address` (`workers/index.ts` mailbox key `mailboxes/{email}.json`; `receiveEmail` uses `allRecipients[0]` since `EMAIL_ADDRESSES=[]`). + +- **Config:** `DOMAINS=nimblerbot.com`; redeployed (version `1fd09c31`, then a `secret put` version). +- **Access:** new self-hosted app `Agentic Inbox (nimblerbot.com)` → `ainbox.nimblerbot.com` (`id=efb1a4c3…`, `POLICY_AUD=814e4b55…`); same `TEAM_DOMAIN`; same `agentic-inbox-bridge` service token (`id=658c03a3…`) added to its 2 policies (Eric email allow + bridge `non_identity`). `POLICY_AUD` written to the Worker secret + Infisical **Agentic Inbox/prod** (old `6189a26a…` overwritten). +- **Custom domain:** `ainbox.nimblerbot.com` → Worker (proxied AAAA `100::`) via Workers Domains API. Apex reserved for mail only — no AAAA/MX coexistence needed (cleaner than the `ai.*` plan). +- **Email Routing:** enabled on the apex (clean — fresh zone, no foreign-MX conflict). Literal rules `sofia.luz@`/`silas.vertiz@ → agentic-inbox`; catch-all disabled (drop). +- **Email Sending:** `nimblerbot.com` onboarded. **Final apex DNS** (12 records): proxied AAAA `ainbox`; 3× MX `route1/2/3.mx.cloudflare.net`; apex SPF `v=spf1 include:_spf.mx.cloudflare.net ~all` (single — no collision); `_dmarc p=reject`; DKIM `cf2024-1._domainkey`; plus `cf-bounce.*` (MX + SPF + DKIM) for sending bounces. +- **Mailboxes:** `sofia.luz@nimblerbot.com` (Sofia Luz), `silas.vertiz@nimblerbot.com` (Silas Vertiz). +- **Validated:** `sofia→silas` (CF-internal, stored on first poll); `sofia→eric@nimblersoft.com` (external — landed in **inbox**, passes `p=reject`); `eric→sofia` (external inbound — stored); global NS/MX/SPF/DKIM/DMARC propagation confirmed via `8.8.8.8` + `1.1.1.1`. +- **Teardown/rollback** (net-new system — no prior mail to revert): delete the 2 routing rules + 2 mailboxes, remove the Access app (`efb1a4c3…`) + custom domain, offboard Email Sending, disable Email Routing. +- **Follow-ups:** (a) decommission the now-moot `ai.nimblersoft.com` Access app (`142c5fd6…`) / custom domain / Email Sending onboarding; (b) optional per-mailbox signatures; (c) Phase 2 bridge (`WEBHOOK_URL=https://mail-bridge.nimblersoft.com/…` not live yet — `notifyBridge` no-ops/logs on failure, so inbound/outbound are unaffected). + +--- + ### Phase 2: Bridge Migration **Goal:** Build webhook-driven bridge between Agentic Inbox and Mattermost. Integrate Hermes `/v1/responses` for persona-aware reply generation. Replace OpenMail dependencies. @@ -459,7 +473,7 @@ After G is unblocked: literal per-mailbox routing rules (`sofia.luz@`/`silas.ver | Phase | Status | Notes | |-------|--------|-------| | Phase 0 | ✅ Completed | Grilling session resolved all architecture decisions | -| Phase 1 | 🟡 In Progress | Live infra A–F done (R2, DOMAINS, Access+service token, secrets, deploy, custom domain, Email Sending). **Step 1.9 inbound Email Routing BLOCKED** (apex-MX wizard — see Phase 1 Execution Notes); mailboxes/validation pending on it. | +| Phase 1 | ✅ Complete | Live + validated on dedicated domain **`nimblerbot.com`** (web UI `ainbox.nimblerbot.com`; inbound + outbound tested internal & external, 2026-06-17). Shared `ai.nimblersoft.com` zone abandoned (apex-MX conflict). Pending follow-ups: decommission `ai.*` resources; optional signatures. | | Phase 2 | ⬜ Not Started | Requires Phase 1 completion. Architecture fully defined. | | Phase 3 | ⬜ Not Started | Requires Phase 2 stable (≥7 days) | | Phase 4 | ⬜ Deferred | Low priority. Activate after Phase 3. | diff --git a/wrangler.jsonc b/wrangler.jsonc index 844267eb..18f85655 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -6,14 +6,14 @@ "observability": { "enabled": true }, - // Web UI is served at ai.nimblersoft.com via a Worker CUSTOM DOMAIN (behind Cloudflare + // Web UI is served at ainbox.nimblerbot.com via a Worker CUSTOM DOMAIN (behind Cloudflare // Access). NOTE: @cloudflare/vite-plugin strips `routes` from the generated deploy config // (build/server/wrangler.json), so custom domains here are NOT applied by `npm run deploy`. // The custom domain is managed out-of-band via the Workers Domains API: - // PUT /accounts/{acct}/workers/domains {zone_id, hostname:"ai.nimblersoft.com", + // PUT /accounts/{acct}/workers/domains {zone_id, hostname:"ainbox.nimblerbot.com", // service:"agentic-inbox", environment:"production"} - // It creates a PROXIED AAAA (100::) that coexists with the ai.nimblersoft.com MX and does - // NOT touch the nimblersoft.com apex. + // It creates a PROXIED AAAA (100::) on the ainbox subdomain; the nimblerbot.com apex is + // reserved for mail (Email Routing MX + Email Sending), kept separate from the UI host. "compatibility_flags": [ "nodejs_compat" ], @@ -28,11 +28,12 @@ // // DOMAINS accepts a single domain or a comma-separated list to serve multiple // domains from one instance, e.g. "example.com,another.com". Each domain needs its - // own Email Routing catch-all rule, and must be verified for outbound sending. - // Phase 1 cuts over ai.nimblersoft.com ONLY. ericmaster.ninja + meliruns.com are - // listed for recognition but remain live on Zoho — their Email Routing/MX cutover is - // deferred to PLAN Phase 4. nimblersoft.com is intentionally absent (Google Workspace). - "DOMAINS": "ai.nimblersoft.com,ericmaster.ninja,meliruns.com", + // own Email Routing rule(s) and must be onboarded for outbound sending. + // Phase 1 runs on the dedicated zone nimblerbot.com (its own CF zone, mail at the + // apex). The shared nimblersoft.com/Google zone was ruled out: CF refuses Email + // Routing while the apex has non-Cloudflare (Google Workspace) MX. ericmaster.ninja + // + meliruns.com remain live on Zoho (deferred to PLAN Phase 4). + "DOMAINS": "nimblerbot.com", // EMAIL_ADDRESSES optionally restricts mailbox creation to specific addresses, and // may span the configured domains, e.g. ["hello@example.com", "hi@another.com"]. "EMAIL_ADDRESSES": []