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

feat(kora): KR-D-DAEMON ST2 — web + MCP + heartbeat listeners#101

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-D-DAEMON-ST2
May 22, 2026
Merged

feat(kora): KR-D-DAEMON ST2 — web + MCP + heartbeat listeners#101
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-D-DAEMON-ST2

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Three concrete listeners plug into the ST1 coordinator. After this PR, `kora daemon` boots a complete admin-UI + MCP + scheduler runtime on the internal port 9119.

Bucket spec: `kora_docs/17_cc_bucket_prompts/KR-D-DAEMON_always_alive_runtime.md` (commit 54032c6).

Base: `feature/phase2-upgrades` — NOT main.

New modules

  • `kora_cli/listeners/init.py` — explicit-order package init (heartbeat → web → mcp registrations fire at import time).
  • `kora_cli/listeners/heartbeat.py` — pure-asyncio `HeartbeatScheduler` with `register_periodic_task(name, interval, callable)` API for Feature 2 to consume. Pre-registers `kora.daemon.alive` placeholder task (30s no-op log). One-task failures don't kill loop; shutdown cancels all + awaits in-flight.
  • `kora_cli/listeners/web.py` — `WebListener` wraps existing `kora_cli.web_server.app` via programmatic uvicorn (Server.serve + should_exit). Binds 127.0.0.1:9119 default; `KORA_WEB_HOST` / `KORA_WEB_PORT` env overrides.
  • `kora_cli/listeners/mcp.py` — NEW MCP HTTP server mounted at `/mcp` on the shared admin app. MINIMAL surface: only `kora__daemon_status` returning coordinator state + listener health + uptime via `DaemonCoordinator.get_status()`. Bearer auth from `KORA_MCP_BEARER_TOKEN`, fail-CLOSED on unset/empty. GET `/mcp/tools/list` (bucket-spec smoke path) + POST `/mcp` (JSON-RPC 2.0). Constant-time bearer compare via `hmac.compare_digest`.

daemon.py additions (+82 lines)

  • `DaemonCoordinator.get_status()` — JSON-serializable snapshot the MCP tool returns: state (`booting`/`running`/`shutting_down`), uptime, shutdown reason, per-listener registered+started state.
  • Module-level `current_coordinator()` accessor — set by `cmd_daemon` during run.
  • `cmd_daemon` lazy-imports `kora_cli.listeners` so listener modules' `register_daemon_listener` calls fire.

Route-mount ordering — caught + fixed

`kora_cli/web_server.py:6412` runs `mount_spa(app)` at module-import time, adding a `/{full_path:path}` SPA catch-all. Plain `app.include_router(router)` adds AFTER it — catch-all wins on `/mcp/*`. Fixed by inserting MCP routes at the FRONT of `app.routes` via a scratch sub-app to resolve dependency wiring first.

Tests (29 new, 44 total all passing)

  • `test_heartbeat.py` — 8 tests: register validation, overwrite semantics, pre-registered alive task, multi-fire periodic, cancellation on shutdown, failing-task isolation, empty scheduler, first-fire-delayed-by-interval (no thundering herd at daemon-start).
  • `test_web.py` — 5 tests: full lifecycle with httpx GET, double-shutdown safe, env overrides, non-int port rejection, defaults.
  • `test_mcp.py` — 16 tests: GET /mcp/tools/list valid/no-bearer/wrong-bearer/malformed/env-unset, POST tools/list, POST tools/call daemon_status, unknown method (-32601), unknown tool (-32602), parse error (-32700), MCPListener.startup fail-CLOSED on unset env, startup fail-CLOSED on whitespace env, startup ok with env, status with no coordinator, status with active coordinator, tool descriptor schema.

End-to-end smoke (live daemon)

```
KORA_DEV=1 KORA_MCP_BEARER_TOKEN=tok KORA_WEB_PORT=9189 kora daemon
```

  • All 3 listeners boot in registration order (heartbeat → web → mcp)
  • `GET /mcp/tools/list` with bearer → 200 + `kora__daemon_status` descriptor
  • `GET /mcp/tools/list` without bearer → 401
  • `POST /mcp tools/call kora__daemon_status` → 200 + live coordinator state JSON showing `listeners: [heartbeat:started, web:started, mcp:started]`
  • SIGTERM → daemon exits 0 (clean LIFO shutdown)

§6 ship checklist

  • Base `feature/phase2-upgrades`
  • Title format `feat(kora): KR-D-DAEMON STn — `
  • Cross-references the bucket file in this PR body
  • Tests pass locally (44/44)
  • No new dependencies (FastAPI + uvicorn already in `web` extra)
  • K-DG: no further drift surfaced; ST1's corrections still hold

What's next

  • ST3: webhook routers (Slack events + email inbound) on the SEPARATE public-port FastAPI app (9118 per PM Q1 ruling); R2 §5 amendment doc at `kora_docs/00_canonical_current_state/r2_amendments.md`; per-IP rate limiting on the public port.
  • KR-D-DEPLOY follow-on: fly.toml port 9118 services block, Dockerfile entrypoint flip, Doppler env mapping (`KORA_MCP_BEARER_TOKEN` to `kora-runtime-substrate`; `KORA_SLACK_SIGNING_SECRET` + `KORA_PUREMAIL_HMAC_SECRET` to `kora-runtime-gateways`).

🤖 Generated with Claude Code

Three concrete listeners plug into the ST1 coordinator. After ST2,
`kora daemon` boots a complete admin-UI + MCP + scheduler runtime
on the internal port 9119.

## New modules

- `kora_cli/listeners/__init__.py` — explicit-order package init.
  Importing the package triggers heartbeat → web → mcp registrations.
- `kora_cli/listeners/heartbeat.py` — pure-asyncio `HeartbeatScheduler`
  with `register_periodic_task(name, interval, callable)` API for
  Feature 2 to consume. Pre-registers `kora.daemon.alive` placeholder
  task (30s no-op log). Failures in one task don't kill the loop;
  shutdown cancels all + awaits in-flight callbacks.
- `kora_cli/listeners/web.py` — `WebListener` wraps existing
  `kora_cli.web_server.app` via programmatic uvicorn (Server.serve
  + should_exit). Binds 127.0.0.1:9119 by default; `KORA_WEB_HOST`
  / `KORA_WEB_PORT` env overrides for tests + local dev.
- `kora_cli/listeners/mcp.py` — NEW MCP HTTP server mounted at `/mcp`
  on the shared admin app. MINIMAL surface per spec: only
  `kora__daemon_status` returning coordinator state + listener health
  + uptime via `DaemonCoordinator.get_status()`. Bearer auth from
  `KORA_MCP_BEARER_TOKEN`, fail-CLOSED on unset/empty. Two surfaces:
  GET `/mcp/tools/list` (bucket-spec smoke test path) + POST `/mcp`
  (JSON-RPC 2.0 for full MCP client compat — handles `tools/list` +
  `tools/call`). Constant-time bearer comparison via `hmac.compare_digest`.

## daemon.py additions (+82 lines)

- `DaemonCoordinator.get_status()` — JSON-serializable snapshot
  consumed by the MCP `kora__daemon_status` tool. Surfaces state
  (`booting`/`running`/`shutting_down`), uptime, shutdown reason,
  per-listener registered+started state.
- Module-level `current_coordinator()` accessor — set by `cmd_daemon`
  during run; tests instantiate coordinators directly without
  polluting global state.
- `cmd_daemon` imports `kora_cli.listeners` (lazy) so listener
  modules' import-time `register_daemon_listener` calls fire.

## Route-mount ordering (caught + fixed)

`kora_cli/web_server.py:6412` runs `mount_spa(app)` at module-import
time, which adds a `/{full_path:path}` SPA catch-all. Plain
`app.include_router(router)` adds AFTER it — catch-all wins on
`/mcp/*`. Fixed by inserting MCP routes at the FRONT of `app.routes`
via a scratch sub-app to resolve dependency wiring first.

## Tests (29 new, all passing — 44 total with ST1)

- `test_heartbeat.py` — 8 tests: register validation, overwrite
  semantics, pre-registered alive task, multi-fire periodic,
  cancellation on shutdown, failing-task isolation, empty scheduler,
  first-fire-delayed-by-interval (no thundering herd).
- `test_web.py` — 5 tests: full lifecycle with httpx GET, double-
  shutdown safe, env overrides, non-int port rejection, defaults.
- `test_mcp.py` — 16 tests: GET /mcp/tools/list valid/no-bearer/
  wrong-bearer/malformed/env-unset, POST tools/list, POST tools/call
  daemon_status, unknown method (-32601), unknown tool (-32602),
  parse error (-32700), MCPListener.startup fail-CLOSED on unset env,
  startup fail-CLOSED on whitespace env, startup ok with env,
  status with no coordinator, status with active coordinator, tool
  descriptor schema.

End-to-end smoke (KORA_DEV=1 KORA_MCP_BEARER_TOKEN=tok kora daemon):
all 3 listeners boot, `/mcp/tools/list` returns the tool, `/mcp` POST
tools/call returns live coordinator state JSON showing
`listeners: [heartbeat:started, web:started, mcp:started]`, SIGTERM
exits 0.

## §6 ship checklist

- [x] PR base `feature/phase2-upgrades`
- [x] PR title `feat(kora): KR-D-DAEMON ST2 — web + MCP + heartbeat listeners`
- [x] Tests pass locally (44/44)
- [x] No new dependencies (FastAPI + uvicorn already in `web` extra)
- [x] K-DG: no further drift surfaced; ST1 corrections still hold

## What's next

ST3: webhook routers (Slack events + email inbound) on the SEPARATE
public-port FastAPI app (9118 per PM Q1 ruling); R2 §5 amendment doc.
Then KR-D-DEPLOY follow-on bucket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit 92e39a6 into feature/phase2-upgrades May 22, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-D-DAEMON-ST2 branch May 22, 2026 04:53
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