feat(safety): workspace-file reads, approval UX, and integration hardening

Engine + integration:
- Reviewer inspection now surfaces the real frozen source of an already-frozen
  workspace script/dependency instead of an empty string, so workspace-resident
  scripts resolve without a needless human defer.
- Guard effectful static tools via an explicit, documented set plus the SDK's
  per-tool needs_approval signal; give the exec/stdin wrappers the same
  idempotency guard as their sibling wrappers.
- Centralize DEFAULT_SAFETY_MODE and share one resume safety-mode rule between the
  CLI and runner so the two cannot drift; type InspectionContext.runner, reuse
  RUNTIME_STATE_DIR_NAME, and drop a dead workdir parameter and a write-only field.

TUI approval experience:
- Approve All drops the run into dangerous mode: it approves the pending call and
  turns review off for the rest of the run, with a standing "review off" status flag.
- The status row shows the owning agent as paused while it waits on a decision.
- Redesigned prompt: a risk + tool header, a collapsible command/reason preview
  that expands (e) and scrolls, and no internal digest, agent, or request ids.

Full Python (1138) and Go suites, ruff, and mypy strix/ pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
oyasumi
2026-08-12 05:33:47 +00:00
co-authored by Claude Opus 4.8
parent 41b7b4f392
commit ccbd8c7b58
37 changed files with 4070 additions and 419 deletions
+26
View File
@@ -115,6 +115,32 @@ async def receive_initial_state(connection: socket.socket) -> None:
complete.add(payload["collection"])
@pytest.mark.asyncio
async def test_state_frame_can_carry_many_concurrent_approvals() -> None:
controller = TuiController(args())
requests = [
asyncio.create_task(
controller.safety_approval_callback(
{
"request_id": f"approval-{index}",
"agent_id": f"agent-{index}",
"action": "x" * 500,
"reason": "y" * 500,
}
)
)
for index in range(80)
]
await asyncio.sleep(0)
server = TuiBackendServer(controller)
encoded = server._encode(envelope("state", {"revision": 1, "state": controller.snapshot()}))
assert len(encoded) > MAX_COMMAND_BYTES
await controller.cancel_pending_safety_approvals()
assert set(await asyncio.gather(*requests)) == {"cancelled"}
@pytest.mark.asyncio
async def test_server_requires_ready_before_state_or_commands() -> None:
backend, child = socket.socketpair()