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

feat(KR-6): Python actor_has_capability helper — replaces assert_kora_can_perform stub, closes D-kr3-st1#12

Merged
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR6-actor-has-capability-helper
May 20, 2026
Merged

feat(KR-6): Python actor_has_capability helper — replaces assert_kora_can_perform stub, closes D-kr3-st1#12
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR6-actor-has-capability-helper

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

KR-6 ships the Python mirror of TS-side actorHasCapability (packages/sea-mcp-server/src/capability-matrix.ts:657) and closes D-kr3-st1-capability-check-deferred. The KR-3 ST1 stub at tools/iso_node.py:assert_kora_can_perform (which always allowed and logged the deviation ID) is replaced with a real check.

Cheapest-unlock follow-on per PM's pipeline-order note: no substrate dependency, ~120 LOC including tests, closes one of our own BUILD_DEVIATIONS entries.

New module: plugins/memory/isokron/capability_check.py

class CapabilityDeniedError(Exception):
    def __init__(self, capability: str, *, reason: str = "")

def actor_has_capability(capability: str) -> bool
def assert_kora_can_perform(capability: str, *, reason: Optional[str] = None) -> None
  • actor_has_capability — O(1) dict lookup against the existing C2 mirror's ACTOR_CAPABILITY_MATRIX_KORA_COLUMN. Returns bool.
  • assert_kora_can_perform — raises CapabilityDeniedError (carrying .capability + .reason) when Kora lacks the cap; KeyError propagates for unknown caps.
  • Unknown capability raises KeyError (fail-LOUD) rather than the TS reference's fail-closed false. Python callers are inside Kora's own codebase, so an unknown cap is a programmer error worth surfacing during testing. TS callers ride through a Sea MCP JSON boundary where unknown caps can arrive from payloads — fail-closed there prevents privilege escalation via typo. Documented in the module docstring.

Stub replacement (4 files)

File Change
plugins/memory/isokron/tools/iso_node.py Local assert_kora_can_perform stub deleted; the public names re-export from ..capability_check so existing callers don't have to change import paths.
plugins/memory/isokron/tools/iso_link.py Imports CapabilityDeniedError + assert_kora_can_perform directly from ..capability_check.
plugins/memory/isokron/tools/__init__.py Re-exports the capability_check public surface (CapabilityDeniedError, actor_has_capability, assert_kora_can_perform).

Dispatcher catch — structured denial envelope

handle_iso_node_tool_call and handle_iso_link_tool_call now catch CapabilityDeniedError and surface it as:

{
  "ok": false,
  "denied": true,
  "capability": "...",
  "reason": "..."
}

Mirrors the deferred-write envelope pattern from KR-2 ST3 / ST4 / KR-3 ST2 — model gets an in-band signal rather than an uncaught exception. Today all 7 tools require caps Kora holds, so this path doesn't fire in normal operation; it's defensive against future cap shifts.

Forward-stability invariant

capability_check.py imports only ACTOR_CAPABILITY_MATRIX_KORA_COLUMN from the C2 mirror — no MCP / DB / network. When K-7 ships kora__read_kora_capability_row and a follow-on KR-N swap replaces the C2 mirror with a fresh-per-call MCP fetch, only that one import line changes — the helper's public surface stays identical.

Tested explicitly:

  • test_module_has_no_network_or_db_imports_at_load_time — checks asyncpg, mcp, httpx, aiohttp absent from module source.
  • test_capability_check_only_imports_the_mirror_dict — checks no imports of provider / connection / reads / scratchpad / events / relationlink.

Test plan

11 new tests in test_capability_check.py, all passing:

actor_has_capability (5):

  • cap_sea_create → True (Sea-tier granted)
  • cap_write_agent_scratchpad → True (Plan 02 primary writer)
  • cap_override_security_or_policy_verdict → False (operator-only Cell C)
  • cap_unbless_convention → False (operator-only, added 2026-05-20)
  • Unknown cap → KeyError with diagnostic message

assert_kora_can_perform (4):

  • Granted → silent return (no raise, no log)
  • Denied → CapabilityDeniedError with .capability + default .reason
  • Caller-supplied reason= overrides default
  • Unknown cap → KeyError propagates (NOT CapabilityDeniedError)

Forward-stability (2):

  • No MCP / DB / network imports at module load
  • Module imports only the C2 mirror

Existing tests updated (3 files, parametrize flipped):

  • test_iso_node_tools.py — deleted test_assert_kora_can_perform_stub_logs_deviation_id; added 3 replacement tests (granted-passes / denied-raises / unknown-raises-keyerror).
  • test_iso_link_tools.py — flipped …_capability_check_logs_deviation to …_passes_silently (asserts no denial envelope + ok=True).
  • test_tool_finalize.py — flipped parametrized test_every_iso_tool_logs_capability_stub_deviation to test_every_iso_tool_passes_capability_check_for_granted_caps (asserts no denial envelope across all 7 tools).

Gates

  • ty check7,337 diagnostics, zero-delta vs KR-3 ST3 baseline.
  • pytest tests/plugins/memory/326/326 passing (11 new + 11 net from refactored deferred-log tests + 304 pre-KR-6).
  • Full suite via xdist (-n auto): 24,649 passed / 142 failed / 129 skipped. Δ vs KR-3 ST3 (24,625/153/129): +24 passed, −11 failed. Same tests/tools/* + tests/skills/* xdist isolation noise; none touch plugins/memory/isokron/.

Rule-6 / BUILD_DEVIATIONS / Open asks

Closed: D-kr3-st1-capability-check-deferred moved from Open to a new Closed section in BUILD_DEVIATIONS.md with the KR-6 spec quote + the forward-stability invariant inline.

4 deferrals remain open pending CC#1 substrate buckets:

Deviation Closes when
D-kr2-st2-capability-matrix-mirror K-7
D-kr2-st3-no-scratchpad-write-mcp-tool K-8
D-kr2-st4-no-chain-emit-mcp-tool K-9
D-kr3-st2-no-relationlink-write-mcp-tool K-10

All four follow the same shape: signature is forward-stable, body swaps from raise <DeferredError> to mcp_client.invoke(...) when the substrate dependency lands.

README "Operator pitfalls" table trimmed from 5 to 4 rows; a "Recently closed" note replaces the entry so operators see the resolution at a glance.

Standing by for the K-7/K-8/K-9/K-10 substrate dispatches → 4 small mechanical follow-on KR-N swap PRs on this lane.

🤖 Generated with Claude Code

KR-6 ships the Python mirror of TS-side actorHasCapability
(packages/sea-mcp-server/src/capability-matrix.ts:657). Replaces the
KR-3 ST1 stub at tools/iso_node.py:assert_kora_can_perform (which
always allowed + logged [kora.isokron.todo]
D-kr3-st1-capability-check-deferred) with a real check.

Cheapest-unlock follow-on per PM's pipeline-order note: no substrate
dependency, ~120 LOC including tests, closes one of our own
BUILD_DEVIATIONS entries.

New plugins/memory/isokron/capability_check.py:
* CapabilityDeniedError — carries .capability + .reason (default
  message names the capability; callers can override).
* actor_has_capability(capability) — O(1) dict lookup against the
  existing C2 mirror's ACTOR_CAPABILITY_MATRIX_KORA_COLUMN. Unknown
  capability raises KeyError (fail-LOUD — typo or parity-test drift;
  the parity guard catches mirror divergence on both sides).
* assert_kora_can_perform(capability, *, reason=None) — raises
  CapabilityDeniedError if Kora lacks the cap; KeyError propagates
  for unknown caps.

Behavior change vs the TS reference: TS is fail-closed (unknown cap
returns false); Python is fail-LOUD (unknown cap raises KeyError).
Rationale documented in the module docstring: Python callers are
inside Kora's own code so unknown caps are programmer errors we
want surfaced during testing; TS callers ride through a Sea MCP JSON
boundary where unknown caps can arrive from payloads — fail-closed
there prevents privilege escalation via typo.

Stub replacement:
* tools/iso_node.py — local stub deleted; the public names re-export
  from ..capability_check so existing callers (iso_link.py + tests
  + dispatcher) don't need to change their import paths.
* tools/iso_link.py — imports CapabilityDeniedError directly from
  ..capability_check; previously transitively via iso_node.
* tools/__init__.py — re-exports capability_check public surface.

Tool-handler integration:
* handle_iso_node_tool_call and handle_iso_link_tool_call now catch
  CapabilityDeniedError and surface a structured envelope:
  {"ok": false, "denied": true, "capability": ..., "reason": ...}
  Mirrors the deferred-write envelope pattern from KR-2 ST3 / ST4 /
  KR-3 ST2 — model gets an in-band signal rather than an uncaught
  exception. (Today, all 7 tools require caps Kora has, so this path
  doesn't fire in normal operation — but it's defensive against
  future cap shifts.)

Tests:
* New tests/plugins/memory/test_capability_check.py (11 tests):
  - actor_has_capability granted (cap_sea_create, cap_write_agent_
    scratchpad)
  - actor_has_capability denied (cap_override_security_or_policy_
    verdict, cap_unbless_convention)
  - actor_has_capability unknown → KeyError with diagnostic message
  - assert_kora_can_perform granted (cap_propose_policy_change)
  - assert_kora_can_perform denied → CapabilityDeniedError with
    .capability + default .reason
  - assert_kora_can_perform with caller-supplied .reason
  - assert_kora_can_perform unknown → KeyError propagates
  - Forward-stability: module has no MCP / DB / network imports at
    load time (the K-7 → KR-N swap invariant — must remain a one-line
    import change).
  - Tightness: capability_check.py imports only the C2 mirror — no
    provider / connection / reads / scratchpad / events / relationlink
    pulls (keeps the swap surface tight).

* Existing KR-3 tests updated (the stub-log assertions no longer
  apply):
  - test_iso_node_tools.py: deleted
    test_assert_kora_can_perform_stub_logs_deviation_id; added
    test_assert_kora_can_perform_passes_for_granted_capability +
    test_assert_kora_can_perform_raises_for_denied_capability +
    test_assert_kora_can_perform_raises_keyerror_for_unknown_cap.
  - test_iso_link_tools.py: renamed/flipped
    test_iso_link_traverse_capability_check_logs_deviation →
    test_iso_link_traverse_capability_check_passes_silently
    (asserts no denial envelope + ok=True).
  - test_tool_finalize.py: parametrized
    test_every_iso_tool_logs_capability_stub_deviation → flipped to
    test_every_iso_tool_passes_capability_check_for_granted_caps
    (asserts no denial envelope across all 7 tools).

BUILD_DEVIATIONS:
* D-kr3-st1-capability-check-deferred moved from Open to a new
  Closed section with the spec quote, the forward-stability
  invariant explained, and the tool-handler integration noted.

README "Operator pitfalls":
* Deferred-surface table trimmed from 5 rows to 4 (D-kr3-st1
  removed); a Recently-closed note replaces the entry so operators
  see the resolution at a glance.

Local gates:
* ty check — 7,337 diagnostics, zero-delta vs KR-3 ST3 baseline.
* pytest tests/plugins/memory/ — 326/326 passing (11 new KR-6 + 11
  new flipped + 304 pre-KR-6).
* Full suite via xdist (-n auto): 24,649 / 142 failed / 129 skipped.
  Δ vs KR-3 ST3 (24,625/153/129): +24 passed, -11 failed (xdist
  isolation oscillates; none touch isokron).

4 deferrals remain open pending CC#1 substrate:
* D-kr2-st2-capability-matrix-mirror ← K-7
* D-kr2-st3-no-scratchpad-write-mcp-tool ← K-8
* D-kr2-st4-no-chain-emit-mcp-tool ← K-9
* D-kr3-st2-no-relationlink-write-mcp-tool ← K-10

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit ad33540 into main May 20, 2026
rafe-walker added a commit that referenced this pull request May 23, 2026
…132)

Pairs with CC#3 KR-FEAT-AI-RESPONSE-LOOP. Operator-facing view of Kora reasoning calls — model used, tokens, duration, errors.

- Backend /api/reasoning/recent stub (4 calls spanning the full cost-ladder × error-taxonomy matrix).
- ReasoningPanel.tsx: 4-column stats strip + filter pills + newest-first timeline with tier-tinted model badges + color-mapped cost-rung pills + duration bars + token counts.
- Dashboard card #12 (Brain icon). Row-2 now 3+3+3+3 — clean 4-row × 3-col closure.

4-layer security contract: plain-text response rendering + Anthropic key walk-payload sweep + PII walk-payload sweep (per-field and whole-blob) + TS interface enforcement.

K-DG drift caught: spec example used uppercase NORMAL/WARN_75/... but agent/cost_state_holder.py:114-117 wire format is lowercase .value strings. Used lowercase in stub so real CC#3 data and the FE pill-color map agree at flip time. Backend test pins lowercase + flags intentional spec divergence.

287/287 admin-panel tests pass across 24 suites; tsc -b + vite build clean.
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