feat(dashboard): wire capability-block approval to real apply (PRD 0016)

Phase 3 of PRD 0016. dashboard.approve() now dispatches to
apply_capability_change when the proposal is a capability-block:

  cred-proxy-block → apply_routes_change
  pipelock-block   → apply_allowlist_change
  capability-block → apply_capability_change   (new in PRD 0016)

CapabilityApplyError joins the ApplyError tuple, so the TUI's key
handlers catch it the same way and surface failures in the status
line.

After a successful capability-block apply, dashboard archives the
proposal+response itself — the supervise sidecar was torn down by
apply_capability_change and can't archive its own queue file.
Without this, dashboard.discover_pending would keep surfacing the
resolved proposal forever.

No audit log for capability-block per PRD 0013 — its record lives
in the per-bottle Dockerfile state + transcript snapshot.

Tests stub apply_capability_change at the dashboard module level,
add TestCapabilityApplyWiring (call wiring, failure-keeps-pending,
no-audit invariant, archive-after-apply), and update TestApproveReject
to stub the capability path too so it stays docker-independent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 05:28:35 -04:00
parent 0899a898e0
commit d9c47d0fbe
2 changed files with 91 additions and 4 deletions
+16 -4
View File
@@ -22,6 +22,10 @@ from datetime import datetime, timezone
from pathlib import Path
from .. import supervise as _supervise
from ..backend.docker.capability_apply import (
CapabilityApplyError,
apply_capability_change,
)
from ..backend.docker.cred_proxy_apply import (
CredProxyApplyError,
apply_routes_change,
@@ -45,6 +49,7 @@ from ..supervise import (
TOOL_CAPABILITY_BLOCK,
TOOL_CRED_PROXY_BLOCK,
TOOL_PIPELOCK_BLOCK,
archive_proposal,
list_pending_proposals,
render_diff,
write_audit_entry,
@@ -56,7 +61,7 @@ from ._common import PROG
# Errors any remediation engine may raise. Caught by the TUI key
# handlers and surfaced in the status line so a failed apply keeps
# the proposal pending rather than crashing curses.
ApplyError = (CredProxyApplyError, PipelockApplyError)
ApplyError = (CredProxyApplyError, PipelockApplyError, CapabilityApplyError)
# --- Discovery -------------------------------------------------------------
@@ -155,9 +160,10 @@ def approve(
diff_before, diff_after = apply_allowlist_change(
qp.proposal.bottle_slug, file_to_apply,
)
# capability-block remediation lands in PRD 0016; until then
# it stays a no-op approval and the audit (none for capability)
# is skipped.
elif qp.proposal.tool == TOOL_CAPABILITY_BLOCK:
diff_before, diff_after = apply_capability_change(
qp.proposal.bottle_slug, file_to_apply,
)
response = Response(
proposal_id=qp.proposal.id,
@@ -170,6 +176,12 @@ def approve(
qp, action=status, notes=notes,
diff_before=diff_before, diff_after=diff_after,
)
if qp.proposal.tool == TOOL_CAPABILITY_BLOCK:
# The supervise sidecar was torn down by apply_capability_change,
# so it can't archive its own proposal+response. Archive here so
# dashboard.discover_pending stops surfacing the resolved
# proposal forever.
archive_proposal(qp.queue_dir, qp.proposal.id)
def reject(qp: QueuedProposal, *, reason: str) -> None: