"EMAIL Genie prefix — inbound subject prefix routes message to Genie (Branded AI) for automated drafting + action proposals"
§1 — Description (BCP-14 normative)
The EMAIL service MUST ship Genie-prefix routing at services/email/src/genie/ matching subject prefix, calling TASK-PORTAL-005 Branded Genie, proposing actions, queueing for user approval, 6 memory audit kinds.
- MUST hook into
services/email/src/inbound_processor.rsafter message stored: callgenie::route(message).
- MUST match prefix per DEC-1590 via
prefix_router.rs::matches(subject, tenant):
- Load
tenant.genie_prefix(default "Genie:"). - Case-insensitive starts-with match after stripping
Re:/Fwd:.
- MUST load context per DEC-1594 — brand pack (TASK-PORTAL-002), CRM contact via TASK-EMAIL-006 link, thread last-10-msgs, tenant tool list from TASK-MCP-006.
- MUST call Branded Genie at
portal_bridge.rs::propose(message, context)— invokes TASK-PORTAL-005 chat with system prompt: "Read this email, propose 0-N actions. Output JSON [{kind, params, rationale}]".
- MUST validate proposed
genie_action_kindagainst closed enum per DEC-1592.
- MUST queue actions for user review per DEC-1593 — NEVER auto-execute.
- MUST define
genie_sessionsandgenie_actionstables at migration0010: ```sql CREATE TABLE genie_sessions ( session_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, message_id UUID NOT NULL, thread_id UUID NOT NULL, status TEXT NOT NULL DEFAULT 'proposing' CHECK (status IN ('proposing','awaiting_review','executing','completed','failed','dismissed')), started_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ, trace_id CHAR(32) ); ALTER TABLE genie_sessions ENABLE ROW LEVEL SECURITY; CREATE POLICY genie_sessions_rls ON genie_sessions USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON genie_sessions FROM cyberos_app; GRANT UPDATE (status, completed_at) ON genie_sessions TO cyberos_app;
CREATE TABLE genie_actions ( action_id UUID PRIMARY KEY, session_id UUID NOT NULL REFERENCES genie_sessions(session_id), tenant_id UUID NOT NULL, kind TEXT NOT NULL CHECK (kind IN ('draft_reply','create_issue','summarize_thread','fetch_data','escalate_human','no_action')), params JSONB NOT NULL, rationale TEXT, status TEXT NOT NULL DEFAULT 'proposed' CHECK (status IN ('proposed','approved','executed','dismissed','failed')), reviewed_by UUID, reviewed_at TIMESTAMPTZ, executed_at TIMESTAMPTZ, result JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE genie_actions ENABLE ROW LEVEL SECURITY; CREATE POLICY genie_actions_rls ON genie_actions USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON genie_actions FROM cyberos_app; GRANT UPDATE (status, reviewed_by, reviewed_at, executed_at, result) ON genie_actions TO cyberos_app; ```
- MUST emit 6 memory audit kinds per DEC-1595. PII per TASK-MEMORY-111: message body + AI output SHA-256 hashed.
- MUST thread trace_id from inbound → prefix match → Genie → action queue → user approve → execute.
- MUST add tenant config column to extend TASK-EMAIL-001: ``
sql ALTER TABLE email_tenant_config ADD COLUMN genie_prefix TEXT DEFAULT 'Genie:'; ALTER TABLE email_tenant_config ADD COLUMN genie_enabled BOOLEAN DEFAULT false;``
- MUST NOT auto-execute action per DEC-1593.
- MUST NOT bypass TASK-MCP-006 tool gating when executing fetch_data action — only allowlisted tools per tenant.
§2 — Why this design
Why prefix routing (DEC-1590)? Lightweight opt-in per-message; doesn't intercept regular email flow.
Why action proposals not direct execution (DEC-1593)? AI hallucination risk on customer-facing sends; manual review is the gate.
Why 6 action kinds (DEC-1591)? Covers common Genie use cases — reply drafts, issue creation, thread summary, data lookup, human escalation, no-op (matches existing CDO workflows).
Why brand pack context (DEC-1594)? Branded Genie must speak in tenant voice; brand pack defines tone+language+disclaimers.
§3 — API contract
GET /v1/email/genie/sessions (list user's pending Genie reviews)
GET /v1/email/genie/sessions/{id} (detail with proposed actions)
POST /v1/email/genie/actions/{id}/approve (execute)
POST /v1/email/genie/actions/{id}/dismiss (mark dismissed)
PUT /v1/email/genie/config (tenant prefix + enabled toggle)
Sample session detail:
{
"session_id": "uuid",
"message_id": "uuid",
"status": "awaiting_review",
"actions": [
{
"action_id": "uuid",
"kind": "draft_reply",
"params": {"body": "Dear John, thanks for reaching out..."},
"rationale": "Customer asked about pricing; drafted standard response.",
"status": "proposed"
},
{
"action_id": "uuid",
"kind": "create_issue",
"params": {"project_id": "...", "title": "Schedule pricing demo for Acme"},
"rationale": "Follow-up needed in 24h.",
"status": "proposed"
}
]
}
§4 — Acceptance criteria
- Subject prefix triggers Genie. 2. Case-insensitive match. 3. Strips Re:/Fwd: before match. 4. Tenant prefix configurable. 5. Genie_enabled toggle respected (off → skip silently). 6. Context loaded (brand + CRM + thread + tools). 7. 6 action kinds enum + cardinality test. 8. Actions queued, never auto-executed. 9. 6 memory audit kinds emitted. 10. PII scrubbed (body/AI output SHA256). 11. RLS denies cross-tenant. 12. Trace_id preserved. 13. fetch_data respects TASK-MCP-006 gating. 14. Approve → execute (calls TASK-EMAIL-009 send / TASK-PROJ-001 create / etc.). 15. Dismiss → status=dismissed (audit). 16. AI failure → status=failed + sev-2. 17. Append-only sessions/actions tables. 18. Multiple actions per session executed in order. 19. Result of execution stored in actions.result. 20. Branded Genie call uses brand pack tone.
§5 — Verification
#[tokio::test]
async fn prefix_triggers_genie() {
let ctx = TestContext::with_genie_enabled().await;
let msg = ctx.receive_inbound_with_subject("Genie: Draft a reply").await;
let session = ctx.fetch_genie_session_for_msg(msg.id).await;
assert!(session.is_some());
}
#[tokio::test]
async fn never_auto_executes() {
let ctx = TestContext::with_genie_enabled().await;
let msg = ctx.receive_inbound_with_subject("Genie: Help me").await;
let session = ctx.wait_for_session_proposing(msg.id).await;
let sent_emails = ctx.email_send_count().await;
let issues = ctx.issue_create_count().await;
assert_eq!(sent_emails, 0);
assert_eq!(issues, 0);
assert_eq!(session.status, "awaiting_review");
}
#[tokio::test]
async fn case_insensitive_prefix() {
let ctx = TestContext::with_genie_prefix("Genie:").await;
let cases = ["GENIE: help", "genie: help", "Genie: help", "Re: GENIE: reply", "Fwd: Genie: forward"];
for subj in cases {
let msg = ctx.receive_inbound_with_subject(subj).await;
assert!(ctx.fetch_genie_session_for_msg(msg.id).await.is_some());
}
}
// 5.4..5.10
§7 — Dependencies
Upstream: TASK-EMAIL-001, TASK-PORTAL-005, TASK-CUO-101. Cross-module: TASK-PORTAL-002 (brand pack), TASK-CRM-001 (contact), TASK-MCP-006 (tool gating), TASK-AI-003 (LLM), TASK-MEMORY-111 (PII).
§8 — Sample payloads (see §3)
§9 — Open questions
None blocking.
§10 — Failure modes
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| Genie disabled (tenant) | flag check | skip silently | inherent |
| Branded Genie unreachable | HTTP timeout | status=failed; sev-2 | retry |
| AI returns invalid action kind | enum match | filter + sev-3 audit | inherent |
| User no email send permission | exec time | action fails, status=failed | request perm |
| Tool not in allowlist (fetch_data) | TASK-MCP-006 gate | rejected | inherent |
| Prefix not configured | use default "Genie:" | inherent | inherent |
| Concurrent prefix match on same msg | UNIQUE on session_id+message_id | first wins | inherent |
| Approve already-executed action | status check | 409 | inherent |
| Brand pack missing | TASK-PORTAL-002 fallback | use default tone | inherent |
| Genie context too large (>50k tokens) | truncate | last 10 msgs only | inherent |
§11 — Implementation notes
- §11.1 Prefix match regex:
^(re:|fwd:)?\s*{escaped_prefix}\s*case-insensitive. - §11.2 Action proposer prompt includes JSON schema for output validation.
- §11.3 Result column stores execution outcome (e.g. sent_message_id, created_issue_id).
- §11.4 memory audit body: action kinds + counts; AI output SHA256.
- §11.5 fetch_data executes via TASK-MCP-006-gated MCP tools; tenant must have allowlisted them.
End of TASK-EMAIL-008 spec.