This repository was archived by the owner on May 26, 2026. It is now read-only.
feat(kora): KR-MCP-AUDIT-ON-DENIAL — emit audit at cap-gate denial#150
Merged
rafe-walker merged 1 commit intoMay 23, 2026
Merged
Conversation
Activates CC#1's pinned-no-alert rule from KR-ALERTS-PANEL-FLIP (#145). Two denial paths now write JSONL audit rows BEFORE returning the -32001 envelope, so the capability_denied_24h alert rule surfaces real warnings when callers are misconfigured. Two helpers in kora_cli/listeners/mcp.py: - _emit_capability_denied_audit — fires at the cap-gate (when _check_cap_gate returns a -32001 envelope). Writes seam=mcp.tool_called with details.result="capability_denied" plus tool_name, tool_kind="mutating", caller_actor_kind, caller_actor_id, required_capability, duration_ms=0, tool_status="not_allowed". - _emit_actor_id_required_audit — fires at the actor_id-required gate inside kora__request_stop (the extra gate from KR-MCP-STOP-CONTROL ST2). Same shape but details.result="actor_id_required" — distinct discriminator so the alert rule's detail_match doesn't conflate two different operator-fix paths (cap-not-granted vs actor_id-field-absent in mcp_callers.yaml). Both helpers are best-effort emit: any exception from the audit sink is caught + logged but does NOT mask the denial response to the caller (verified by test_cap_gate_denial_audit_emitted_ before_envelope). Reason text NEVER in the denial audit (caller-supplied string might leak context) — same posture as the stop-tool's success-path audit from #147. Documentation: - kora_cli/alerts/aggregator.py docstring: removed the forward-compat "rule emits zero today" block; replaced with a description of the now-live wiring + the two result discriminators. Tests: - tests/kora_cli/test_listeners/test_mcp_audit_on_denial.py: 8 new tests covering cap-gate denial audit shape, missing actor_id audit shape, audit-emitted-before-envelope (sink failure doesn't mask denial), executor-not-invoked-on- denial, no-denial-audit-on-success, and the SECURITY sweep (bearer token never in any audit row). - tests/kora_cli/alerts/test_aggregator.py: renamed test_capability_denied_today_no_alert_since_audit_doesnt_emit_denials → test_capability_denied_today_emits_alert_when_threshold_exceeded. Flipped behavior to assert the alert fires when 11+ capability_denied audit rows accumulate in 24h. The new test also seeds 7 "ok" + 5 "actor_id_required" rows to verify the rule's detail_match correctly excludes them (alert count is 11, not 23). Full regression: 9097 passed; 47 failed identical to ST2 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. After this lands, the alerts panel surfaces real "N capability_denied responses in 24h" warnings when callers are misconfigured. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
rafe-walker
added a commit
that referenced
this pull request
May 23, 2026
…152) CI determinism restored. 77 insertions across 3 files — test-side only, no production changes. Diagnostic findings: 1. Two leak sources identified — both from CC#3 prior buckets: - test_mcp_tools_stop_control.py (PR #142, ST1) — 13 sites set h_mod._HOLDER = holder with no teardown - test_mcp_audit_on_denial.py (PR #150, ST3) — 1 success-path test does the same 2. Why xdist surfaces it: xdist workers are separate processes (no cross-worker bleed), but within a worker tests run sequentially in scheduler order — and xdist worksteal scheduler does not preserve serial mode alphabetic file ordering that happens to put handlers/ before test_listeners/. 3. Why serial does not flake: alphabetic ordering means email handler tests always precede the mutating test_listeners tests in serial mode. Fix: three autouse _reset_operational_state_holder fixtures — two plug the leak sources, the third is defensive belt-and-suspenders on the email handler tests so a future leak source cannot re-poison them. Pattern documented in fixture docstrings + PR body. Production code (the _HOLDER module-level singleton) untouched — spec §3 explicit non-scope. Test-side fix only. 30/30 xdist verification runs all green; 5/5 spec-mandated runs all green; zero new failures vs ST3 baseline.
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
Activates CC#1's pinned forward-compat alert rule from KR-ALERTS-PANEL-FLIP (#145). The `capability_denied_24h` rule already greps `mcp.tool_called` audit JSONL for `details.result == "capability_denied"`, but the cap-gate at `listeners/mcp.py:_check_cap_gate` returned -32001 before ever reaching the audit emit — so the rule was emitting zero alerts. This PR wires the audit emit at the gate; the rule now fires when the threshold is exceeded.
Two denial paths audited
Both write `seam=mcp.tool_called` BEFORE returning the -32001 envelope. Best-effort emit (sink failure is caught + logged but does not mask the denial response — verified by `test_cap_gate_denial_audit_emitted_before_envelope`).
Schema (both denial paths)
Reason text NEVER in audit (caller-supplied string risk) — same posture as the stop-tool's success-path audit.
CC#1 pinned test updated
`tests/kora_cli/alerts/test_aggregator.py`:
Test plan
After this lands, the alerts panel surfaces real "N capability_denied responses in 24h" warnings when callers are misconfigured.
🤖 Generated with Claude Code