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
Conversation
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>
This was referenced May 22, 2026
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.
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
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/recentbody 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
GET /api/email/recentreturning 4 representative messages deliberately spanninginbound (received)+outbound (sent_ok)+filtered_non_allowlist+inbound-with-attachmentso 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 → tolabel badge, status badge (only when non-default — happy-path rows stay clean), spoofing-risk chip (destructive tone whenspoofing_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 surfacesmessage_id,in_reply_to, full timestamp, has_html note pointing to Purelymail web client for full HTML rendering.Mailicon. Layout: keepinglg:grid-cols-3per PM preference — row 3 wraps cleanly to its second line (3+3+3+1 → 3+3+4). Headline goes destructive whenfiltered_non_allowlist > 0OR anyspoofing_warningfires in the window ORsent_failed > 0ORhandler_error > 0./email+ nav entry withMailicon (between/slack-dmand/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).
from_label/to_labelare LABELS —joshua/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 araw_from_addressdiagnostic or address-in-subject gets caught.message_idis 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.body_text_truncated_400rendered as PLAIN TEXT via React's default child escaping. Frontend pin:EmailPanel.tsxnever usesdangerouslySetInnerHTMLfor message bodies — real bodies may contain arbitrary HTML / phishing payloads. Full HTML rendering happens in the Purelymail web client, NOT here.has_htmlis metadata only.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 (nodangerouslySetInnerHTML, body as JSX child, spoofing chip rendering),by_direction_24hreconciliation, cron-regression sanity.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 --noEmitclean.pnpm vite buildclean (direct invocation, see notes below)./email, exercise filters, expand rows, verify spoofing chip + destructive card tone.Notes
pnpm build(=tsc -b && vite build) surfaces 4 PRE-EXISTING errors inHeartbeatPanel.tsx+DashboardPage.tsx. TheHeartbeatStatusenum gained"unknown"andHeartbeatService.last_check_atbecame nullable in a recent change, but the consumers weren't updated. Confirmed present on barefeature/phase2-upgrades— NOT 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.lg:grid-cols-3, which wraps to 3+3+4 visually as the row continues. Not bumping tocols-4per PM preference forcols-3; the wrap looks fine./admin/email; using flat/emailto match every prior panel in this branch.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