This repository was archived by the owner on May 26, 2026. It is now read-only.
feat(kora): KR-FEAT-SLACK-DM ST1 — inbound handler + identity check + JSONL log#119
Merged
rafe-walker merged 1 commit intoMay 22, 2026
Merged
Conversation
… JSONL log First ST of Phase 2 Feature 5. Fills the no-op handler body in the Slack webhook router from KR-D-DAEMON ST3 (#104) with real Kora-side DM processing: identity filtering, operational-state gating, JSONL persistence, structured-log emit. ST1 is INBOUND-only. ST2 adds the outbound `chat.postMessage` echo reply via a new SlackClient. ## New modules - **`kora_cli/handlers/__init__.py`** — new package for per-source handlers (distinct from `listeners/` which owns lifecycle + transport routing). - **`kora_cli/handlers/slack_dm_handler.py`** (~270 lines) — `SlackDMHandler.handle_event(payload)` returns `{"ok": True}` to Slack regardless of internal disposition (so Slack doesn't retry). Filter precedence: 1. **State gate** — `OperationalStateHolder.current.primary_state` is PAUSED or STOPPED → drop. (PrimaryState enum values verified against `agent/operational_state.py:70-74` per KR-MCP-RUNTIME-SURFACE ST1 K-DG correction.) 2. **Event type** — only regular `message` (no subtype). `message_changed` / `message_deleted` / `app_mention` etc. → filtered_subtype. 3. **Channel type** — only `im`. `channel` / `group` → filtered_non_im. 4. **Bot ID** — if `event.bot_id` is set → filtered_bot (echo-loop defense if Kora's own bot ever ends up in the chat). 5. **Identity** — `event.user` must match `KORA_SLACK_JOSHUA_USER_ID` env. Mismatch → filtered_non_joshua (silently dropped; we do NOT echo back to non-Joshua, per spec). **Fail-CLOSED**: if `KORA_SLACK_JOSHUA_USER_ID` env is unset, ALL messages are dropped with `reason: "joshua_id_env_unset"` + a WARN log. Operator must configure the env to enable any routing. ## JSONL persistence - Path: `${KORA_SLACK_DM_LOG_PATH}` env override, else `get_kora_home() / "slack_dm_log.jsonl"` (honors `KORA_HOME` primary + legacy `HERMES_HOME` fallback per `kora_constants`). - Schema per line: `{received_at, channel_id, thread_ts, user_id, text, event_ts, handled_status, extra?, error?}`. - `handled_status` enum: `received` / `filtered_non_joshua` / `filtered_non_im` / `filtered_bot` / `filtered_subtype` / `dropped_paused` / `dropped_stopped` / `handler_error`. - Best-effort: OSError on write → WARN log + continue (never block the 200 to Slack). - **SECURITY**: signing secret + bot token NEVER touch the JSONL. Handler never imports either env var. Test asserts the secret env value doesn't appear in the log file after a diverse sequence of events. ## Structured-log emit `[kora.slack_dm.received]` log line on identified Joshua DMs. Same audit-seam pattern as KR-D-DAEMON ST3 webhook dead-letter + KR-MCP-RUNTIME-SURFACE ST2 tool-called audit — when substrate ships a `kora.slack_dm.received` chain-event vocab literal, the runtime extension is small (the log emit is the stable seam). Non-Joshua events do NOT emit the chain-event log — only identified-as-Joshua + state-passes-gate. ## Webhook router wire-in `kora_cli/listeners/webhooks.py:_handle_slack` (+31 lines): - Routes verified `event_callback` payloads to `SlackDMHandler` - Belt-and-suspenders exception guard at the listener boundary (the handler wraps its own `_handle_event_inner` too) — Slack ALWAYS gets a 200, retries are aggressive + expensive - Dead-letter on handler construction / exception path - URL-verification handshake unchanged (still inline) ## §4 PM-opens — all PM defaults accepted (per chat ack) - **Q1** JSONL location: single file at `get_kora_home() / "slack_dm_log.jsonl"`. Per-day rotation is operator-managed (logrotate / fluent-bit / etc.). ✓ - **Q2** PAUSED behavior: drop (no buffer-replay). ✓ - **Q3** Echo format `"Kora received: {text[:200]}"`: locked for ST2. ✓ - **Q4** Dual signing-secret env note: will land in ST3 runbook. ✓ ## Tests (20 new, 197 total all passing) **`test_slack_dm_handler.py`** (20 tests): - Joshua DM received + logged + chain-event-log emit - Thread continuity (thread_ts in JSONL) - Non-Joshua filtered + NO chain-event emit - JOSHUA_USER_ID env unset → fail-CLOSED drop ALL - PAUSED state drops Joshua DMs - STOPPED state drops - READY + ACTIVE states DO NOT drop - Bot message filtered (echo-loop defense) - Subtype event filtered - Non-message event (app_mention) filtered - Non-IM channel_type filtered (`channel`, `group`) - Handler internal exception → handler_error + still 200 - Malformed payload (missing event dict) survives - JSONL: one valid JSON per line, required fields present - `received_at` is round-trippable ISO 8601 - **SECURITY**: signing-secret env value never in JSONL after diverse event sequence (5 payloads exercising every filter) - Log path env override + default resolution via `get_kora_home()` ## §5 ship checklist - [x] Base `feature/phase2-upgrades` - [x] Title `feat(kora): KR-FEAT-SLACK-DM STn — <scope>` - [x] §4 PM-opens resolved (all defaults accepted) - [x] Slack signing secret + bot token NEVER logged (asserted) - [x] Identity check fail-CLOSED (non-Joshua → drop, no reply) - [x] OperationalState gating respected (test for PAUSED + STOPPED + READY + ACTIVE) - [x] K-DG: `OperationalStateHolder.current` is a `@property` (not method) per KR-MCP-RUNTIME-SURFACE ST1 correction; PrimaryState lowercase string values verified. - [x] Tests pass locally (**197/197** across full daemon + listener + handler + docker suite) ## What's next **ST2** — outbound DM via NEW `kora_cli/clients/slack_client.py` with `chat.postMessage`; echo reply `"Kora received: {text[:200]}"`; retry-on-429 (respect retry-after) + 1 retry on 5xx; dead-letter on final failure; outbound entries in same JSONL file. AI-driven response generation lands in `KR-FEAT-SLACK-DM-AI` follow-on. **ST3** — `kora_docs/15_status_and_roadmap/slack_app_setup_runbook.md` covering Slack app creation, OAuth scopes, Event Subscriptions wire-up, Doppler secret setup, smoke test, troubleshooting + the dual signing-secret env transition note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rafe-walker
added a commit
that referenced
this pull request
May 22, 2026
Phase 2 Feature 5 frontend. Pairs with CC#3 KR-FEAT-SLACK-DM (#119). - Backend /api/slack-dm/recent stub (4 messages spanning happy path + filtered_non_joshua). - SlackDMPanel.tsx (stats + filter pills + newest-first timeline + expandable rows). - Dashboard card #9 (MessageCircle icon) + nav + 15 backend tests. 4-layer security contract (extending standard 3-layer with a 4th walk-payload guard for Slack-specific tokens): no raw U... Slack user IDs + channel_id stub shape + plain-text rendering (with dangerouslySetInnerHTML ban) + Slack token/signing-secret walk-payload sweep. Layout: 9 cards = clean 3+3+3 with lg:grid-cols-3 — landed the symmetry sweet spot, no orphans. 219/219 admin-panel tests pass across 20 suites; tsc + vite clean.
This was referenced May 22, 2026
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
First ST of Phase 2 Feature 5. Fills the no-op handler body in the Slack webhook router from KR-D-DAEMON ST3 (#104) with real Kora-side DM processing: identity filtering, operational-state gating, JSONL persistence, structured-log emit.
ST1 is INBOUND-only. ST2 adds the outbound `chat.postMessage` echo reply via a new SlackClient.
Bucket spec: `kora_docs/17_cc_bucket_prompts/KR-FEAT-SLACK-DM_kora_joshua_dm.md` (commit e734d61).
Base: `feature/phase2-upgrades` — NOT main.
New modules
Filter precedence
Fail-CLOSED: if `KORA_SLACK_JOSHUA_USER_ID` env is unset, ALL messages are dropped with `reason: "joshua_id_env_unset"` + WARN log. Operator must configure to enable any routing.
JSONL persistence
Structured-log emit
`[kora.slack_dm.received]` log line on identified Joshua DMs ONLY. Same audit-seam pattern as KR-D-DAEMON ST3 webhook dead-letter + KR-MCP-RUNTIME-SURFACE ST2 tool-called audit — when substrate ships a `kora.slack_dm.received` chain-event vocab literal, the runtime extension is small (log emit is the stable seam).
Webhook router wire-in
`kora_cli/listeners/webhooks.py:_handle_slack` (+31 lines):
§4 PM-opens — all defaults accepted (per chat ack)
Tests (20 new, 197 total all passing)
§5 ship checklist
What's next
🤖 Generated with Claude Code