Add real-time per-tool-call audit streaming (HARNESS-WP-0002-T03)

Claude Code executes its own tools internally in --print mode -- there
is no way for a caller to externally dispatch individual tool calls
without abandoning that self-contained agent model. What
--output-format stream-json --include-hook-events does allow: observing
each tool_use/tool_result/hook event in real time.

- adapter.py: AgenticClaudeCodeAdapter gains an optional on_tool_event
  callback; streaming mode (Popen + background reader thread) is used
  only when set, blocking subprocess.run path is unchanged otherwise.
- runner.py: run_task gains emit_tool_events/on_tool_event, collecting
  events onto RunResult.tool_events and posting a tool_call hub event
  per tool when reporting is enabled.
- cli.py: --stream-tool-events flag on `run`, prints each event as a
  tagged JSON line ahead of the unchanged final result block.

Live-verified against the real claude CLI: 5 real tool events streamed
correctly (2x Bash, 1x Write) plus Stop hook lifecycle events, real
commit landed, final result block unchanged. 13 new tests
(test_adapter.py + 2 in test_runner.py), all passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-26 14:49:36 +02:00
parent c77a643393
commit 6c4f00e2c2
7 changed files with 384 additions and 18 deletions

View file

@ -87,21 +87,51 @@ repo's `runner.py`/`adapter.py` to expose it, so glas-harness can call
into `rein-aharness` instead of `rein-aharness` only running itself via
its own CLI/poll loop.
**Partially done, live-proven at the coarse level (2026-07-26):**
glas-harness's `glas_harness/reins/rein_aharness.py` implements the
contract today by shelling out to `agent-harness run --task-file ...`
as one opaque `dispatch_tool` call — proven live end-to-end (real
`ext.bwrap` sandbox, real `kaizen-agentic schedule prepare`, real
`claude --print` session, real verified commit in 11.4s). **Still
open:** this repo does not itself expose per-tool-call hooks — glas-harness
cannot yet intercept/policy-check individual tool calls mid-session,
only the whole run as a unit. That deeper refactor (runner.py driving
the Claude Code session turn-by-turn rather than one `claude --print`
call) is what remains of this task.
**Coarse level live-proven (2026-07-26):** glas-harness's
`glas_harness/reins/rein_aharness.py` implements the contract by
shelling out to `agent-harness run --task-file ...` as one opaque
`dispatch_tool` call — proven live end-to-end (real `ext.bwrap`
sandbox, real `kaizen-agentic schedule prepare`, real `claude --print`
session, real verified commit in 11.4s).
**Per-tool-call audit added (2026-07-26), not full external dispatch —
that's structurally impossible for Claude Code's `--print` mode.**
Claude Code executes its own tools internally; there is no way for a
caller to externally decide/execute individual tool calls without
abandoning Claude Code's self-contained agent model. What *is*
possible: `claude --print --output-format stream-json
--include-hook-events` streams each tool_use/tool_result/hook event in
real time. Added:
- `adapter.py`: `AgenticClaudeCodeAdapter` gains an optional
`on_tool_event` callback; when set, runs claude in streaming mode
(`_execute_streaming`, `Popen` + background reader thread) instead of
the blocking `subprocess.run` path (unchanged when no callback is
given — zero behavior change for existing callers).
- `runner.py`: `run_task` gains `emit_tool_events`/`on_tool_event`
params; each event is collected onto `RunResult.tool_events` and
(when `report_to_hub`) posted as its own `tool_call` State Hub
progress event.
- `cli.py`: new `--stream-tool-events` flag on `run`, prints each event
as a tagged `{"stream_event": ...}` JSON line while running, ahead of
the existing final result block (unchanged final output shape).
- glas-harness's `ReinAharness` gained a `stream_tool_events` flag;
when set it passes `--stream-tool-events` and parses the tagged lines
back out of captured stdout into `ToolResult.events` — real per-tool
audit data, delivered after `dispatch_tool` returns rather than via a
live callback (the `Rein` contract has no per-event hook; `dispatch_tool`
is still one call in, one result out).
Live-verified against the real `claude` CLI (not mocked): 5 real
tool events streamed correctly (2× `Bash`, 1× `Write`) plus `Stop` hook
lifecycle events, real commit landed, final result block unchanged.
13 new tests in rein-aharness (`test_adapter.py` + 2 in
`test_runner.py`), 2 new tests in glas-harness (`test_rein_aharness.py`)
— all passing, all mocked except the one live CLI run above.
```task
id: HARNESS-WP-0002-T03
status: progress
status: done
priority: high
state_hub_task_id: "228e999c-807b-4456-a286-4e3ab4fc8e90"
```