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

feat(kora): KR-MCP-STOP-CONTROL ST2 — stop tool + kora_control writer + actor_id#147

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-MCP-STOP-CONTROL-ST2
May 23, 2026
Merged

feat(kora): KR-MCP-STOP-CONTROL ST2 — stop tool + kora_control writer + actor_id#147
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-MCP-STOP-CONTROL-ST2

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Closes the KR-MCP-STOP-CONTROL arc. 3 deliverables in one PR — the two ST1-flagged blockers + the substrate-backed stop tool.

D1 — actor_id field on Caller dataclass + YAML loader

kora_cli/listeners/mcp_caller_auth.py. Optional uuid string on every caller entry. Additive + backwards-compatible: existing callers (no `actor_id` key) load with `actor_id=None`. Loader rejects malformed UUIDs fail-CLOSED (skip + WARN); accepts unhyphenated form and normalizes.

D2 — `kora_cli/clients/kora_control_writer.py`

Python wrapper around `public.issue_kora_control` SECDEF.

Bucket-spec drift caught + corrected: spec said `await isokron_client.invoke("substrate__issue_kora_control", {...})` via IsoKronMCPClient. But `packages/sea-mcp-server/src/tools/` registers no `kora_control` MCP tool, and `KoraControlReader` docstring (lines 16-26) explicitly says "no MCP wrapper exists" — the canonical access path is raw asyncpg through the IsoKron pool (same as the existing reader). Writer mirrors that pattern.

  • dry_run short-circuits before substrate; predicted shape uses zero-UUID + sequence=-1.
  • 10s `asyncio.wait_for` ceiling per §4 Q3. SECDEF's own 60s `statement_timeout` is the substrate-side backstop.
  • Fail-CLOSED on: missing actor_id, invalid `(level, kind)`, missing pool, `asyncpg.PostgresError` → `SubstrateRejected` (sqlstate preserved), client timeout → `KoraControlWriterTimeout`.

D3 — `kora__request_stop` MCP tool

L1 (intake-stop, kind='pause') / L2 (drain, kind='drain') only.

  • JSON schema constrains level to `[1, 2]`; executor ALSO refuses L3-L5 with an operator-on-machine message.
  • `confirm_token` must equal daemon's `daemon_session_id` — newly added on DaemonCoordinator at construction (hex uuid4, per-process stable) + surfaced via `get_status()`. Read `kora__daemon_status` first to fetch.
  • Caller cap `kora__request_stop` is distinct from `kora__request_pause` — verified by test (operator can grant pause/resume without granting stop).
  • `actor_id` required: `-32001 actor_id_required_for_stop` (new `_ST2_ActorIdRequired` error class with dedicated handler in mcp.py).
  • Audit: `{level, kind, dry_run, actor_id_present, result_tag}`. Reason text NEVER in audit per §4 Q4 ruling.
  • Returns immediately after substrate write; enforcement is async via Kora's poll loop.

§4 PM-opens — answers in flight + locked

Q Answer
Q1 SECDEF VERIFIED at `packages/db/migrations/0090_kora_control_secdefs.sql`, substrate commit `2abf095` (May 21 2026). Full 8-arg signature pasted into `kora_control_writer.py` docstring.
Q2 actor_id operator-pastes from substrate `actor_registry` SELECT (must resolve substrate-side; bare `uuidgen` would fail the SECDEF's actor_registry lookup).
Q3 timeout `asyncio.wait_for(..., timeout=10.0)`. SECDEF's 60s statement_timeout is the substrate backstop.
Q4 audit `{level, kind, dry_run, actor_id_present, result_tag}` only. Reason text omitted.

Bonus drift logged

`Caller` is `@dataclass(frozen=True, slots=True)`, not Pydantic — extended the dataclass additively rather than refactor to Pydantic. Same intent, zero footprint.

Tests

  • 14 writer-level unit tests in `tests/kora_cli/clients/test_kora_control_writer.py` — dry_run, missing actor_id, invalid level/kind, no pool, SubstrateRejected wrap (with synthetic PostgresError), 10s timeout wrap, `current_kora_control_writer()` accessor in all 3 states.
  • 16 tool-level tests in `tests/kora_cli/test_listeners/test_mcp_tools_stop.py` — L1/L2 success, L3-L5 refused (parametrized), L0 refused, confirm_token mismatch, empty token, no capability, no actor_id, dry_run, substrate-error, no coordinator, no provider, audit-omits-reason, SECURITY: bearer-token-never-in-envelope (asserted across confirm-mismatch + substrate-error paths).
  • 9 new caller-auth tests in `tests/kora_cli/test_listeners/test_mcp_caller_auth.py` — default-None, valid UUID, malformed UUID skip, non-string skip, unhyphenated canonicalization, mixed entries.

Test plan

  • All 57 new tests pass: `pytest tests/kora_cli/test_listeners/test_mcp_caller_auth.py tests/kora_cli/clients/test_kora_control_writer.py tests/kora_cli/test_listeners/test_mcp_tools_stop.py`
  • Full `tests/kora_cli/test_listeners/` + `tests/kora_cli/clients/` regression green (373 passed)
  • Full `tests/kora_cli + tests/agent` regression: 9051 passed; 47 failed identical to ST1 baseline (zero new regressions; all failures pre-existing in test_anthropic_adapter / test_web_server)
  • Descriptors surface in `/mcp/tools/list` with `requires_cap_gate=True` + `dev_only=False`
  • SECURITY: caller bearer token never appears in any error envelope (confirm-mismatch + substrate-error paths)
  • Reason text never in audit
  • L3-L5 refused at both schema layer (Pydantic enum) and executor layer (defensive)

After this PR merges, Kora has full pause/resume/stop accessible via `/mcp` with caller-attributed substrate writes. Operator retains `kora_control` SQL + flyctl/Doppler paths; the agent surface complements rather than replaces.

🤖 Generated with Claude Code

… + actor_id

Closes the KR-MCP-STOP-CONTROL arc. Three deliverables in one PR:

  D1. actor_id field on Caller dataclass + mcp_callers.yaml loader
      - Optional uuid string. Backwards-compatible — existing
        callers (no actor_id key) load with actor_id=None.
      - Loader rejects malformed UUIDs (fail-CLOSED skip + WARN).
      - Accepts unhyphenated form; normalizes to canonical form.

  D2. kora_cli/clients/kora_control_writer.py — Python wrapper
      around public.issue_kora_control SECDEF (substrate
      packages/db/migrations/0090_kora_control_secdefs.sql,
      shipped substrate-side at commit 2abf095, May 21 2026).
      - Raw asyncpg through the IsoKron pool, mirroring
        KoraControlReader's pattern. Bucket-spec drift: spec
        said "IsoKronMCPClient.invoke('substrate__issue_kora_
        control', ...)" but no MCP wrapper exists in
        sea-mcp-server/src/tools/; the canonical access path
        for this SECDEF is direct asyncpg.
      - dry_run mode short-circuits before substrate; predicted
        shape uses zero-UUID placeholders + sequence=-1.
      - 10s client-side asyncio.wait_for ceiling
        (ISSUE_KORA_CONTROL_TIMEOUT_SECONDS) per §4 Q3.
      - Fail-CLOSED on: missing actor_id, invalid (level, kind),
        missing pool, asyncpg PostgresError (→ SubstrateRejected
        with sqlstate preserved), client-timeout (→ Writer
        Timeout).
      - Module-level current_kora_control_writer() accessor
        bound to the active IsoKronMemoryProvider singleton.

  D3. kora__request_stop MCP tool — L1/L2 only.
      - JSON schema constrains level to [1, 2]; executor ALSO
        refuses L3-L5 with explicit operator-on-machine message.
      - confirm_token must equal daemon's daemon_session_id
        (newly added on DaemonCoordinator at construction +
        surfaced via get_status()). Mismatch → -32602.
      - Caller cap-gate on "kora__request_stop". DISTINCT from
        kora__request_pause (verified by test) — operator can
        grant pause/resume without granting stop.
      - actor_id required: -32001 actor_id_required_for_stop
        (new error class _ST2_ActorIdRequired in mcp_tools.py
        with dedicated handler in mcp.py).
      - Audit emit: {level, kind, dry_run, actor_id_present} —
        reason text NEVER in audit details per §4 Q4 ruling.
      - Returns immediately after substrate write succeeds;
        enforcement is async via Kora's poll loop.

Additional surface:
  - DaemonCoordinator._daemon_session_id (hex uuid4, per-process
    stable) — exposed via .daemon_session_id property + the
    get_status() JSON key. Read by kora__daemon_status for the
    confirm_token mint flow.
  - _jsonrpc_error extended to accept optional `data` field for
    structured error envelopes (used by the actor_id_required
    branch to surface caller_actor_kind + remediation text).

§4 PM-opens resolved:
  Q1 — SECDEF verified at packages/db/migrations/0090_kora_
       control_secdefs.sql; full signature pasted into
       kora_control_writer.py module docstring §1.
  Q2 — operator-pastes actor_id from substrate actor_registry
       (must be substrate-resolvable per SECDEF body checks).
  Q3 — 10s wait_for ceiling, SECDEF's 60s statement_timeout is
       the substrate-side backstop.
  Q4 — {level, kind, dry_run, actor_id_present, result_tag}
       audit keys. Reason text omitted.

Tests:
  - tests/kora_cli/clients/test_kora_control_writer.py: 14
    writer-level unit tests (dry_run, missing actor_id,
    invalid level/kind, no pool, SubstrateRejected wrap,
    timeout wrap, current_writer accessor states).
  - tests/kora_cli/test_listeners/test_mcp_tools_stop.py: 16
    request_stop tool tests (L1/L2 success, L3-L5 refused,
    L0 refused, confirm_token mismatch, empty token,
    no capability, no actor_id, dry_run, substrate-error,
    no coordinator, no provider, audit-omits-reason, SECURITY
    bearer-never-in-envelope).
  - tests/kora_cli/test_listeners/test_mcp_caller_auth.py: 9
    new tests for actor_id parsing (default-None, valid UUID,
    malformed UUID, non-string, unhyphenated, mixed entries).

Full regression: 9051 passed (matches base+57 new). 47 failed
identical to ST1 baseline = zero new regressions. All failures
are pre-existing in tests/agent/test_anthropic_adapter.py +
tests/kora_cli/test_web_server*.py — unrelated to this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit 2345d51 into feature/phase2-upgrades May 23, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-MCP-STOP-CONTROL-ST2 branch May 23, 2026 20:38
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