This repository was archived by the owner on May 26, 2026. It is now read-only.
KR-EMAIL-OUTBOUND-COMPOSE-TOOL — Kora can email operator (R3 Q8a outbound)#179
Merged
rafe-walker merged 1 commit intoMay 24, 2026
Conversation
…(R3 Q8a outbound) Completes the email-surface story: R3 Q8a's *"the other way mostly, like 'Kora, email me that pdf etc.'"*. Companion to PR #176 (inbound → Sea_Ticket). New module ---------- `kora_cli/tools/email_to_operator.py`: * `send_email_to_operator(subject, body, attachments)` — pure Python orchestrator. Recipient PINNED to `KORA_EMAIL_JOSHUA_ADDRESS` (executor itself; never caller- controlled). Reads attachment files off disk into the existing `Attachment` shape; PurelymailClient handles SMTP + its own attachment caps. * Hourly cap module-level deque (mirrors KR-INTENT-EMAIL-TO- SEA-TICKET's pattern); default 5/hr. * Per-rejection wire-stable reason codes (recipient_env_unset / subject_too_long / etc.) * Audit-soft, fail-soft — every failure path returns a structured dict; the reasoning engine sees `status` + `reason` / `error` and adapts. MCP wiring ---------- `kora_cli/listeners/mcp_tools.py`: * New ST2 tool `kora__send_email_to_operator` descriptor + `_dispatch_send_email_to_operator` that thinly wraps the pure-Python module. `requires_cap_gate: True` for external callers (default-deny via cap_matrix); Kora's reasoning loop bypasses cap_matrix via the reasoning-allowlist (below). Reasoning-loop scope expansion ------------------------------ `kora_cli/reasoning/tool_registry.py`: * Added `kora__send_email_to_operator` to `REASONING_TOOL_ALLOWLIST` — the FIRST mutating tool to enter the reasoning surface. Module docstring documents the deliberate scope expansion: the original exclusion of `kora__send_email` was driven by mass-send-via-caller- recipient risk + Loop-risk; the operator-pinned variant neutralizes both (recipient locked, hourly cap). * `execute_reasoning_tool` now routes the allowlisted mutating tool through `ST2_TOOL_DISPATCH` with a synthetic `Caller(actor_kind="kora_reasoning_self", allowed_caps=frozenset({tool_name}))` — minimal cap so a future caller.allows() check fails closed for any other tool. * `get_reasoning_available_tools()` now pulls from both `TOOL_DESCRIPTORS` and `ST2_TOOL_DESCRIPTORS` so the new tool is advertised to Claude. Audit ----- New seam `tool.email_to_operator_sent` (next to `probe.wake_requested` + `intent.email_to_sea_ticket`). One entry per invocation with wire-stable `status` ∈ {sent, rejected, smtp_failure}. Body content never in audit (only sizes); recipient not needed (always operator). System prompt ------------- `kora_docs/00_canonical_current_state/kora_system_prompt.md` — added the tool to the "Tool use" section with usage guidance + an exception note in the mutation-boundary section. Cap_matrix coord ---------------- Resolved inline. cap_matrix is Kora-side config (per-caller `allowed_caps` in mcp_callers.yaml), NOT a substrate-side table. External MCP callers see the new tool but cap-gated default-deny; operator can grant the cap per-caller if needed. Kora's own reasoning loop bypasses cap_matrix via the in-process REASONING_TOOL_ALLOWLIST. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
rafe-walker
added a commit
that referenced
this pull request
May 24, 2026
…n completion) (#182) Unified-operator-interface vision completion. Kora's investigation can now include 'I tried restart_machine on i-abc; result: success; new health state: ok' instead of just 'recommend you rotate the token.' 2nd mutating tool in REASONING_TOOL_ALLOWLIST (documented as 'Deliberate scope expansion #2'). Blast-radius bounded by 3 fail-CLOSED gates: per-probe env + envelope whitelist + per-probe executor target verification. Zero envelopes ship enabled by default. fly restart_machine wired but KORA_PROBE_AUTOFIX_FLY_ENABLED defaults OFF. Executor re-reads env on EVERY call (mid-investigation env-flip closes the gate). supabase/vercel/sentry/doppler envelopes remain explicitly empty. DM differentiation: enabled path includes 'tried/before/after' outcome; disabled path includes the exact manual flyctl command + how to enable. Operator-empathetic engineering. Reason field recorded VERBATIM (unlike #179's body redaction) — operator triage of 'what did Kora decide and why' is primary use case. New audit seam tool.probe_autofix_attempted with status enum {attempted, rejected, execution_failed}. 24 new autofix tests + 317 cross-bucket regression + ruff clean.
5 tasks
rafe-walker
added a commit
that referenced
this pull request
May 24, 2026
…eam in cockpit (#183) Symmetric to PR #180. Surfaces tool.email_to_operator_sent audit seam from #179. Per-seam endpoint /api/outbound-email/recent + page OutboundEmailLogPage.tsx. 3-source drift-guard pin for status enum. K-DG drift caught + privacy reality adopted: spec said 'subject CAN be shown' but #179 was MORE privacy-conscious than spec claimed — audit carries only subject_chars / body_chars (lengths), NOT subject string / body text / recipient. Stricter reality adopted. Pinned by test_privacy_response_never_contains_subject_or_body_text + test_page_does_not_render_subject_or_body_text. Shared-utility extraction flagged: 5 overlapping helpers/components with PR #180 (Sparkline / SummaryChips / FilterChips / formatters / BadgeTone type). Recommend follow-on KR-FE-AUDIT-PANEL-KIT-REFACTOR (3-consumer threshold — covered by the next bucket). 23/23 new tests + 47/47 combined with #180 + tsc + vite build clean.
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
R3 Q8a's "the other way mostly, like 'Kora, email me that pdf etc.'" — the outbound counterpart to PR #176's inbound Sea_Ticket save. Kora's reasoning engine now has a tool to compose and send an email to the operator when the response is too long, structured, or attachment-heavy for Slack DM.
Bucket spec:
17_cc_bucket_prompts/KR-EMAIL-OUTBOUND-COMPOSE-TOOL_kora_emails_operator.md.Cap_matrix coord — RESOLVED INLINE ✅
The spec's STOP-ASK condition ("cap_matrix.kora row needs substrate-team coordination") does not apply — cap_matrix is Kora-side config (per-caller
allowed_capsinmcp_callers.yaml+ per-toolrequires_cap_gateflag), not a substrate-side table.kora__send_email_to_operatorships withrequires_cap_gate: True— external MCP callers are default-deny; operators can grant the cap per-caller viamcp_callers.yamlif they ever want to.REASONING_TOOL_ALLOWLISTinkora_cli/reasoning/tool_registry.py. This PR adds the tool to that allowlist alongside the 5 existing read-only tools.No substrate-team coord needed. No IsoKron PM message required.
Security: why recipient pinning lets this enter the reasoning loop
The original
tool_registry.pydocstring deliberately excludedkora__send_emailfrom the reasoning surface ("Kora already responds via the Slack DM channel; meta-sends would be confusing + Loop-risky") — the real underlying risk was caller-controlled recipients opening a mass-send vector.kora__send_email_to_operatorneutralizes this: the executor itself pins recipient toKORA_EMAIL_JOSHUA_ADDRESS; the tool input schema has notofield. Loop-risk is addressed byKORA_EMAIL_OUTBOUND_HOURLY_CAP(default 5/hr).The deliberate scope expansion is documented in the updated
tool_registry.pymodule docstring + the system prompt's mutation-boundary section. The other mutating tools (request_state_transition, create_sea_ticket, send_slack_dm, send_email, request_pause/resume/stop, send_test_alert) stay excluded — seetest_allowlist_excludes_caller_controlled_mutating_tools.Env vars added
KORA_EMAIL_OUTBOUND_HOURLY_CAP50disables.KORA_EMAIL_OUTBOUND_MAX_ATTACH_MB20KORA_EMAIL_JOSHUA_ADDRESSKORA_PUREMAIL_SMTP_USERNAMEfrom_addr— read by the executor.Sample audit entry
Success path:
{ "emitted_at": "2026-05-23T22:14:09.412000+00:00", "seam": "tool.email_to_operator_sent", "details": { "subject_chars": 24, "body_chars": 1841, "attachment_count": 1, "attachment_total_bytes": 184132, "status": "sent", "smtp_message_id": "<20260523221409.kora@stormhavenenterprises.com>", "sent_at": "2026-05-23T22:14:09Z" }, "caller_session_id": "mcp:kora_reasoning_self", "source": "reasoning" }Rejection path:
{ "emitted_at": "...", "seam": "tool.email_to_operator_sent", "details": { "subject_chars": 220, "body_chars": 1841, "attachment_count": 0, "status": "rejected", "rejection_reason": "subject_too_long", "rejection_detail": {"subject_chars": 220, "max_chars": 200} }, "caller_session_id": "mcp:kora_reasoning_self", "source": "reasoning" }Body content never appears in audit — only sizes. Recipient field absent (always operator).
Sample reasoning-engine invocation
When Joshua DMs "kora email me a summary of today's probe activity with the audit JSONL attached", Kora's reasoning loop would emit a tool_use block like:
{ "type": "tool_use", "name": "kora__send_email_to_operator", "input": { "subject": "[Kora] Probe activity summary — 2026-05-23", "body": "Here's today's rundown:\n\n- 4 probes ran...\n\n(full audit attached)", "attachments": [ {"filename": "audit-2026-05-23.jsonl", "content_path": "/var/lib/kora/audit-export.jsonl"} ] } }Tool result returned to Claude:
{ "status": "sent", "smtp_message_id": "<...>", "sent_at": "2026-05-23T22:14:09Z", "attachment_count": 1, "attachment_total_bytes": 184132 }Files
kora_cli/tools/__init__.py+kora_cli/tools/email_to_operator.py(~480 lines)tests/kora_cli/tools/test_email_to_operator.py(22 tests)kora_cli/listeners/mcp_tools.py— new tool descriptor + dispatcher (thin wrapper around the pure-Python module)kora_cli/reasoning/tool_registry.py— add to allowlist + ST2 dispatch path with synthetic Caller + pull from both descriptor listskora_cli/audit/jsonl_sink.py— newtool.email_to_operator_sentSeamName Literal entrykora_docs/00_canonical_current_state/kora_system_prompt.md— usage guidance + mutation-boundary exception notetests/kora_cli/reasoning/test_anthropic_engine_tool_use.py— bump 5→6 tool count assertions; broaden allowlist-exclusion test to cover all forbidden mutating toolsTest plan
ruff checkclean on all changed files.🤖 Generated with Claude Code