Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-EMAIL-PANEL — email inbox/outbox view stub#121

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-EMAIL-PANEL
May 22, 2026
Merged

feat(kora): KR-EMAIL-PANEL — email inbox/outbox view stub#121
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-EMAIL-PANEL

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Operator-facing view of recent inbound + outbound email exchanges in the cockpit. Pairs with CC#1's KR-FEAT-EMAIL bucket (Feature 3 backend, in flight) — ST2 will swap the stub /api/email/recent body for a real read of ${HERMES_HOME}/email_inbound_log.jsonl + email_outbound_log.jsonl (Purelymail-backed).

Shape is pinned by the new test suite so the FE shipping in this PR keeps rendering correctly during the cut-over.

What's in here

  • Backend stub: GET /api/email/recent returning 4 representative messages deliberately spanning inbound (received) + outbound (sent_ok) + filtered_non_allowlist + inbound-with-attachment so the operator's first look surfaces the filtering posture AND the attachment-count affordance.
  • EmailPanel.tsx (new): title + stub banner + stats strip + filter pills (all / inbound / outbound / filtered / errors) + newest-first timeline. Per row: direction arrow (←/→), from → to label badge, status badge (only when non-default — happy-path rows stay clean), spoofing-risk chip (destructive tone when spoofing_warning: true), attachment count chip, HTML-metadata chip (<Code2> icon — flags "original body contained HTML; rendered as plain text here"), subject (mono), body excerpt (truncated to 80 chars collapsed; full 400-char preview expanded). Expanded detail surfaces message_id, in_reply_to, full timestamp, has_html note pointing to Purelymail web client for full HTML rendering.
  • Dashboard card feat(KR-3 ST2): iso_link_* typed-edge tool family #10 ("Email ↔ Joshua") with Mail icon. Layout: keeping lg:grid-cols-3 per PM preference — row 3 wraps cleanly to its second line (3+3+3+1 → 3+3+4). Headline goes destructive when filtered_non_allowlist > 0 OR any spoofing_warning fires in the window OR sent_failed > 0 OR handler_error > 0.
  • Route /email + nav entry with Mail icon (between /slack-dm and /mcp-clients).

4-layer security contract

Extending the established pattern with email-specific token sweep — every panel touching a new sensitive-data domain adds the domain-specific guard (per PM standardization).

  1. from_label / to_label are LABELSjoshua / kora / unknown_sender, never raw email addresses. Backend test pins per-field AND walk-whole-payload against [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,} — a future leak via a raw_from_address diagnostic or address-in-subject gets caught.
  2. message_id is a STUB shape (stub-msg-id-N) in v1. Real Purelymail message IDs hashed/truncated by CC#1 when real data flips. Backend test pins the stub pattern.
  3. body_text_truncated_400 rendered as PLAIN TEXT via React's default child escaping. Frontend pin: EmailPanel.tsx never uses dangerouslySetInnerHTML for message bodies — real bodies may contain arbitrary HTML / phishing payloads. Full HTML rendering happens in the Purelymail web client, NOT here. has_html is metadata only.
  4. Walk-the-whole-payload guards: Purelymail token hints (KORA_PUREMAIL_*, puremail_*, purelymail_*), HMAC-secret shapes (32+ hex), bearer-token shapes (24+ base64, Bearer X / Authorization: headers) — backend bug or future log-entry edit that leaks creds gets caught at the API edge.

Test plan

  • tests/kora_cli/test_web_server_email.py — 16 tests: shape, 4-message stub pin (with attachment), direction+status diversity, per-entry schema (incl. 400-char body cap), all 4 security guards, FE source-pins (no dangerouslySetInnerHTML, body as JSX child, spoofing chip rendering), by_direction_24h reconciliation, cron-regression sanity.
  • Full admin-panel regression: 241/241 across 21 suites (skipping test_web_server_mcp_clients.py — PM task Fix nous refresh token rotation failure in case where api key mint/retrieval fails NousResearch/hermes-agent#269).
  • pnpm tsc --noEmit clean.
  • pnpm vite build clean (direct invocation, see notes below).
  • Manual smoke: load /email, exercise filters, expand rows, verify spoofing chip + destructive card tone.

Notes

  • Pre-existing build issue flagged: pnpm build (= tsc -b && vite build) surfaces 4 PRE-EXISTING errors in HeartbeatPanel.tsx + DashboardPage.tsx. The HeartbeatStatus enum gained "unknown" and HeartbeatService.last_check_at became nullable in a recent change, but the consumers weren't updated. Confirmed present on bare feature/phase2-upgradesNOT introduced by this PR. Vite alone builds clean, so the production artifact works. Recommend a separate cleanup bucket alongside test task Fix nous refresh token rotation failure in case where api key mint/retrieval fails NousResearch/hermes-agent#269.
  • Layout choice: 10 cards = 3+3+3+1 in lg:grid-cols-3, which wraps to 3+3+4 visually as the row continues. Not bumping to cols-4 per PM preference for cols-3; the wrap looks fine.
  • Convention divergence from spec: spec hints /admin/email; using flat /email to match every prior panel in this branch.
  • Body cap: backend pre-truncates body to 400 chars (body_text_truncated_400); FE does not fetch full bodies — operator pulls from Purelymail web client if full HTML rendering is needed.

🤖 Generated with Claude Code

Operator-facing view of recent inbound + outbound email exchanges
in the cockpit. Pairs with CC#1's KR-FEAT-EMAIL (Feature 3 backend,
in flight) — ST2 will swap the stub /api/email/recent body for a
real read of ${HERMES_HOME}/email_inbound_log.jsonl +
email_outbound_log.jsonl (Purelymail-backed). Shape is pinned by
the new test suite so the FE shipping in this PR keeps rendering
during the cut-over.

Single-PR scope:
  * GET /api/email/recent stub — 4 representative messages
    deliberately spanning inbound (received), outbound (sent_ok),
    filtered_non_allowlist, and inbound-with-attachment so the
    operator's first look surfaces the filtering posture AND the
    attachment-count affordance. stub:true keeps the FE banner
    visible.
  * EmailPanel.tsx — title + stub banner + stats strip + filter
    pills (all / inbound / outbound / filtered / errors) +
    newest-first timeline. Per row: direction arrow, from→to
    label badge, status badge (non-default only), spoofing-risk
    chip (destructive tone when flag set), attachment count
    chip, HTML-metadata chip, subject (mono), body excerpt
    (truncated to 80 in collapsed; full 400-char preview in
    expanded). Expanded detail surfaces message_id, in_reply_to,
    full timestamp, has_html note pointing to Purelymail web
    client for full HTML rendering.
  * Dashboard card #10 (Mail icon). Layout: row 3 grows from 1
    card to 2 (3+3+3+1 → 3+3+4 once row 3 wraps to its second
    line). Keeping lg:grid-cols-3 per PM preference; row 3 wraps
    cleanly. Headline goes destructive when filtered_non_allowlist
    > 0 OR any spoofing_warning fires OR sent_failed > 0 OR
    handler_error > 0.
  * Route /email + nav entry between /slack-dm and /mcp-clients.

4-layer security contract (extending the established pattern with
email-specific token sweep):
  1. from_label / to_label are LABELS (joshua / kora /
     unknown_sender) — NEVER raw email addresses. Backend test
     enforces per-field AND walk-whole-payload sweep against
     `[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}` regex.
  2. message_id is a STUB shape (`stub-msg-id-N`) in v1. Real
     Purelymail IDs hashed/truncated by CC#1 when real data flips.
     Backend test pins the stub pattern.
  3. body_text_truncated_400 rendered as PLAIN TEXT via React's
     default child escaping. Frontend pin: EmailPanel.tsx never
     uses dangerouslySetInnerHTML for message bodies — real
     bodies may contain arbitrary HTML / phishing payloads. Full
     HTML rendering happens in the Purelymail web client, NOT
     here. has_html is metadata only.
  4. Walk-the-whole-payload guards: Purelymail token hints
     (KORA_PUREMAIL_*, puremail_*, purelymail_*), HMAC-secret
     shapes (32+ hex), bearer-token shapes (24+ base64,
     "Bearer X" headers) — backend bug or future log-entry edit
     that leaks creds gets caught at the API edge.

Tests:
  * tests/kora_cli/test_web_server_email.py — 16 tests covering
    shape, 4-message stub pin (with attachment), direction+status
    diversity, per-entry schema (including 400-char body cap),
    all 4 security guards (per-field + walk-payload sweeps for
    addresses / tokens / secrets / message_id shape), FE
    source-pins (no dangerouslySetInnerHTML, body rendered as
    JSX child, spoofing chip), by_direction_24h reconciliation,
    cron-regression sanity.
  * Full admin-panel regression: 241/241 across 21 suites
    (skipping test_web_server_mcp_clients.py — PM task NousResearch#269).
  * tsc --noEmit clean. `vite build` clean directly.

Pre-existing build issue flagged: `tsc -b` (strict project-
references mode used by `pnpm build`) surfaces 4 PRE-EXISTING
errors in HeartbeatPanel.tsx + DashboardPage.tsx — HeartbeatStatus
enum gained "unknown" and HeartbeatService.last_check_at became
nullable in a recent change, but the consumers weren't updated.
Confirmed present on bare feature/phase2-upgrades (NOT introduced
by this PR). Vite build itself succeeds, so the production
artifact is fine. Recommend a separate cleanup bucket alongside
the test_web_server_mcp_clients.py task NousResearch#269.

Refs: rafe-walker/kora-docs 17_cc_bucket_prompts/KR-EMAIL-PANEL_inbox_view_stub.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit f5b076c into feature/phase2-upgrades May 22, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-EMAIL-PANEL branch May 22, 2026 17:07
rafe-walker added a commit that referenced this pull request May 23, 2026
Backend endpoint swap. Reads both email_inbound_log.jsonl (#138) + email_outbound_log.jsonl (#124), merges, projects to existing EmailMessage TS shape. FE auto-flips (stub badge gone).

- kora_cli/web_server.py: -106 stub + 330 LOC live endpoint + 2 projection helpers + status-mapping table.
- NEW tests/kora_cli/test_web_server_email_panel_flip.py: 596 LOC, 32 tests covering projection per direction + merge/sort + limit param + malformed tolerance + security walk-payload with message_id carve-out.

4-layer security + message_id carve-out: walk-payload sweep excludes only message_id + in_reply_to fields from email-regex check (RFC 5322 <id@operator-domain> is legitimate). test_no_email_addresses_outside_message_id_carve_out pins the boundary.

Status taxonomy collapse: handler filtered_paused/filtered_stopped → FE dropped_paused; filtered_non_joshua → filtered_non_allowlist. JSONL stays canonical; operator panel sees coarser FE union.

Fixture-isolation discipline applied: caught real cross-test bleed where original PR #121 fixture only patched upstream namespace; added 3-namespace get_kora_home monkeypatch per CC#2 #137 lesson.

Spoofing semantic flip: spoofing_warning = NOT entry.spoofing_check_skipped. Test pins behavior when future bucket adds real envelope-based detection.

47 new + 185 cross-bucket regression. Stub badge auto-disappears.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant