Diagrams — The tau-security Package

Course: 2A — Building AI Harnesses for Cybersecurity Package: tau-security

These six diagrams accompany the teaching document (01-teaching-document.md). They cover tau's three-layer architecture, the transformation from coding agent to security agent, scope enforcement, evidence chaining, the offensive state machine, and the tool execution lifecycle.


Diagram 1 — tau's Three-Layer Architecture

flowchart TB
    subgraph TAU_AI["tau_ai — provider/model streaming layer"]
        PROV[ModelProvider<br/>OpenAI · Anthropic · OpenRouter<br/>HF · Google · Mistral · local]
        PEVT[Provider events:<br/>ResponseStart · TextDelta<br/>ThinkingDelta · ResponseEnd<br/>Error · Retry]
        PROV --> PEVT
    end

    subgraph TAU_AGENT["tau_agent — the portable brain (zero UI/CLI deps)"]
        H[AgentHarness<br/>harness.py · 298 LOC<br/>owns transcript + queues + listeners]
        L[run_agent_loop<br/>loop.py · 276 LOC<br/>pure stateless provider/tool loop]
        T[AgentTool<br/>tools.py · 78 LOC<br/>frozen dataclass + async executor]
        E[14 AgentEvents<br/>events.py · 134 LOC<br/>the layer contract]
        S[Session JSONL<br/>append-only · parent_id branching<br/>tamper-evidence foundation]
        H -->|delegates| L
        L -->|emits| E
        L -->|calls| T
        L -->|appends to| S
    end

    subgraph TAU_CODING["tau_coding — the coding app"]
        CT[read · write · edit · bash<br/>glob · grep<br/>1,057 LOC of tools]
        TUI[TUI · Textual<br/>CLI · slash commands<br/>skills · on-disk sessions]
        CT --> TUI
    end

    subgraph TAU_SECURITY["tau_security — the security app (sibling)"]
        ST[port_scan · http_probe · code_scan<br/>record_finding · generate_report<br/>scope-checked executors]
        SCOPE[Scope<br/>hard-wired legal boundary<br/>assert_in_scope on every call]
        CHAIN[EvidenceChain<br/>SHA-256 hash chain<br/>tamper-evident]
        TRIAGE[TriagePipeline<br/>candidate → confirmed | rejected]
        RPT[Report<br/>JSON + HTML · integrity verified]
        ST --> SCOPE
        ST --> CHAIN
        CHAIN --> TRIAGE
        TRIAGE --> RPT
    end

    PEVT -->|provider.stream_response| L
    E -->|AgentEvent stream<br/>the contract| TUI
    E -->|AgentEvent stream<br/>same contract| ST

    style TAU_AI fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style TAU_AGENT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TAU_CODING fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style TAU_SECURITY fill:#1a0a0a,stroke:#e65c00,color:#e4e4e8
    style H fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style L fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style T fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style PROV fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style PEVT fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style CT fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style TUI fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style ST fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style SCOPE fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style CHAIN fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style TRIAGE fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style RPT fill:#1a0a0a,stroke:#e65c00,color:#e65c00

Reading: tau's load-bearing design decision is the boundary between tau_agent (the portable brain) and the app layers (tau_coding, tau_security). The brain depends only on tau_ai and Pydantic — it has zero knowledge of Textual, Rich, local config paths, slash commands, rendering, scope, or evidence. The AgentEvent stream is the contract: tau_coding's TUI subscribes to it to render status lines; tau_security's evidence chain subscribes to it to capture ToolExecutionEndEvent as hash-chained records. Same brain, same event stream, two completely different apps. The brain does not know which app is driving it. This is why tau-security is a sibling package that imports from tau_agent rather than a fork — it reuses the brain verbatim and swaps only the app layer. The numbers are the actual source line counts (harness.py 298, loop.py 276, tools.py 78, events.py 134). The entire brain is ~786 lines; the security layer adds ~600.


Diagram 2 — The tau_coding → tau_security Transformation

flowchart LR
    subgraph SAME["What stays the same (the brain)"]
        BRAIN[AgentHarness<br/>same transcript ownership]
        LOOP[run_agent_loop<br/>same 276-line pure loop]
        TOOL_ABS[AgentTool abstraction<br/>same frozen dataclass]
        EVENTS[14 AgentEvents<br/>same event stream]
        STEER[steer / follow_up<br/>same HITL mechanism]
        SESSION[Session JSONL<br/>same append-only durability]
    end

    subgraph CHANGED["What changes (the harness)"]
        direction TB
        PROMPT["System prompt<br/>coding assistant<br/>↓<br/>offensive state machine<br/>Recon→Hypothesis→Exploit<br/>→Evidence→Triage→Report"]
        TOOLS["Tools<br/>read · write · edit · bash<br/>↓<br/>port_scan · http_probe · code_scan<br/>record_finding · generate_report"]
        SCOPE_CH["Scope<br/>none<br/>↓<br/>hard-wired legal boundary<br/>assert_in_scope on every call"]
        EVIDENCE_CH["Evidence<br/>session history<br/>↓<br/>tamper-evident SHA-256 hash chain"]
        OUTPUT_CH["Output<br/>code changes<br/>↓<br/>client-ready security report<br/>JSON + HTML · integrity verified"]
    end

    BRAIN -.->|unchanged| PROMPT
    LOOP -.->|unchanged| TOOLS
    TOOL_ABS -.->|unchanged| SCOPE_CH
    EVENTS -.->|unchanged| EVIDENCE_CH
    STEER -.->|unchanged| OUTPUT_CH

    style SAME fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CHANGED fill:#1a0a0a,stroke:#e65c00,color:#e4e4e8
    style BRAIN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style LOOP fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TOOL_ABS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EVENTS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style STEER fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SESSION fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style PROMPT fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style TOOLS fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style SCOPE_CH fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style EVIDENCE_CH fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style OUTPUT_CH fill:#1a0a0a,stroke:#e65c00,color:#e65c00

Reading: This diagram is the course thesis made visual. The left column is the brain — everything that stays identical when you transform a coding agent into a security agent. The AgentHarness, run_agent_loop, the AgentTool abstraction, the 14 event types, the steering mechanism, the session durability: none of these change. The right column is the harness — the four things that do change. The system prompt encodes the offensive state machine. The tools swap from file/shell operations to security operations. The scope goes from nonexistent to a hard-wired code-level gate. The evidence goes from a session transcript to a tamper-evident hash chain. The output goes from code changes to a client-ready report. Four changes, none touching the brain. This is what "the model is 1.6% of the system, the harness is the other 98.4%" means in practice: the brain (which includes the model invocation) is reused verbatim; all the security-specific engineering lives in the harness layer.


Diagram 3 — Scope Enforcement Flow

flowchart TB
    START([Model calls a security tool<br/>e.g. port_scan host=10.0.0.5]) --> EXTRACT[Extract target from arguments<br/>host = arguments 'host']
    EXTRACT --> GATE{assert_in_scope<br/>scope, target, tool_name}

    GATE -->|deny-rule matches| BLOCK ScopeViolation raised
    GATE -->|no allow-rule matches| BLOCK
    GATE -->|rate limit exceeded| BLOCK

    BLOCK --> BLOCKED_RESULT[_blocked result<br/>ok=False · data: blocked=True<br/>operation NEVER executes]
    BLOCKED_RESULT --> END([Return AgentToolResult])

    GATE -->|in-scope + rate OK| RECORD[scope.record_request<br/>increment host counter]
    RECORD --> EXEC[Execute the operation<br/>nmap · urllib · file scan]
    EXEC --> CAPTURE[_capture_evidence<br/>Evidence.capture → chain.add_evidence]
    CAPTURE --> DETECT{auto-detect issues?<br/>SQL errors · missing headers<br/>500 status · vuln patterns}
    DETECT -->|issue found| FINDING[Finding.create<br/>chain.add_finding<br/>chain.link_evidence]
    DETECT -->|no issue| SUCCESS
    FINDING --> SUCCESS[_success result<br/>ok=True · data: evidence_id<br/>evidence_hash · finding_id]
    SUCCESS --> END

    style START fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style EXTRACT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style GATE fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style BLOCK fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style BLOCKED_RESULT fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style RECORD fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EXEC fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CAPTURE fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style DETECT fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style FINDING fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style SUCCESS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style END fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8

Reading: This is the structural defense. Every target-touching tool call flows through this path. The model calls port_scan with a host argument. The executor extracts the target and calls assert_in_scope(scope, target, tool_name). Three things can fail the gate: a deny-rule matches (explicit exclusion), no allow-rule matches (target was never in scope), or the rate limit is exceeded (engagement is causing impact). On any failure, a ScopeViolation is raised, caught by the executor, and returned as a _blocked result — the operation never executes. The model receives a structured result telling it the call was blocked and why. On success, the request is recorded (rate tracking), the operation executes, evidence is captured into the hash chain, and any auto-detected issues become Finding records linked to that evidence. The critical property: the scope check happens BEFORE the operation, in code, regardless of what the model was told to do via prompt injection. This is why tau-security is not vulnerable to the InjecAgent ~50% failure rate for out-of-scope actions — the injection can steer the model, but the structural gate stops the action.


Diagram 4 — Evidence Chain Hash Linking

flowchart LR
    GENESIS(["genesis<br/>_previous_hash = 'genesis'<br/>(initial state)"])

    subgraph E1["Evidence 1"]
        E1_RAW["raw: nmap output<br/>sha256: hash(content_1)"]
        E1_CHAIN["chain_hash =<br/>SHA256('genesis' + ':' + hash_1)"]
    end

    subgraph E2["Evidence 2"]
        E2_RAW["raw: HTTP response<br/>sha256: hash(content_2)"]
        E2_CHAIN["chain_hash =<br/>SHA256(chain_hash_1 + ':' + hash_2)"]
    end

    subgraph EN["Evidence N"]
        EN_RAW["raw: code snippet<br/>sha256: hash(content_n)"]
        EN_CHAIN["chain_hash =<br/>SHA256(chain_hash_n-1 + ':' + hash_n)"]
    end

    GENESIS -->|input to| E1_CHAIN
    E1_CHAIN -->|input to| E2_CHAIN
    E2_CHAIN -->|...| EN_CHAIN
    EN_CHAIN --> HEAD(["chain_head_hash<br/>= _previous_hash<br/>(final chain_hash_n)"])

    subgraph VERIFY["verify_integrity — recomputes from genesis"]
        V1["previous = 'genesis'"]
        V2["for each record:<br/>expected = SHA256(previous + ':' + record.sha256)<br/>match? → previous = expected"]
        V3["final previous == chain_head_hash?<br/>(catches truncation)"]
        V1 --> V2 --> V3
    end

    HEAD -.->|compared against| V3

    subgraph TAMPER["Tamper scenario"]
        T1["Attacker modifies Evidence 2.raw"]
        T2["Evidence 2.sha256 no longer matches stored"]
        T3["Evidence 2.chain_hash recomputes differently"]
        T4["Evidence 3+ chain_hash all break<br/>(cascade)"]
        T5["verify_integrity → False"]
        T1 --> T2 --> T3 --> T4 --> T5
    end

    style GENESIS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E1 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E2 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E1_RAW fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E1_CHAIN fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style E2_RAW fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E2_CHAIN fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style EN_RAW fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EN_CHAIN fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style HEAD fill:#0d1b2a,stroke:#e65c00,color:#e4e4e8
    style VERIFY fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style V1 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style V2 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style V3 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TAMPER fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T1 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T2 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T3 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T4 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T5 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8

Reading: The evidence chain is append-only, and each record's hash incorporates the previous record's hash. The chain starts at "genesis" — a fixed initial string, not a real hash. Evidence 1's chain hash is SHA-256("genesis:" + content_hash_1). Evidence 2's chain hash is SHA-256(chain_hash_1 + ":" + content_hash_2). Each record depends on all previous records. The tamper scenario (bottom) shows what happens when an attacker modifies Evidence 2's raw field: its sha256 no longer matches the stored value, its chain_hash recomputes differently, and every subsequent record's chain_hash breaks in a cascade. verify_integrity() walks the chain from genesis, recomputing each hash, and returns False at the first mismatch. It also checks that the final recomputed hash matches the stored chain_head_hash (_previous_hash) — this catches truncation, where evidence was appended after the recorded head. This is the property that makes the report legally defensible: the integrity_verified field in the JSON/HTML report is not a claim, it is a recomputable proof. A client receiving export_evidence("evidence.json") can re-run the verification and confirm the chain is unbroken. This is what separates a security harness from a scanner — proof, not just detection.


Diagram 5 — The Offensive State Machine as System Prompt

flowchart LR
    RECON["1. RECON<br/>Map the target surface<br/><br/>port_scan → open ports<br/>http_probe → endpoints<br/>Build target inventory"]

    HYP["2. HYPOTHESIS<br/>Form vuln hypotheses<br/><br/>What services are exposed?<br/>What versions?<br/>What endpoints look interesting?"]

    EXPLOIT["3. EXPLOIT<br/>Test hypotheses<br/><br/>http_probe with params<br/>→ injection, auth bypass<br/>code_scan on local files<br/>→ static vulns"]

    EVID["4. EVIDENCE<br/>Document confirmed issues<br/><br/>record_finding<br/>severity · location · description<br/>confidence · CWE<br/>Every finding MUST have<br/>linked evidence"]

    TRIAGE["5. TRIAGE<br/>Filter false positives<br/><br/>candidate → confirmed | rejected<br/>dedup by title+location<br/>has_evidence check<br/>known-FP filter<br/>confidence ≥ 0.6"]

    REPORT["6. REPORT<br/>Client deliverable<br/><br/>generate_report<br/>JSON or HTML<br/>scope · methodology<br/>findings table<br/>evidence hashes<br/>triage summary"]

    RECON --> HYP --> EXPLOIT --> EVID --> TRIAGE --> REPORT

    REPORT -.->|feedback loop<br/>gaps found| RECON

    subgraph RULES["Critical rules encoded in the prompt"]
        R1["SCOPE IS ABSOLUTE<br/>out-of-scope = blocked automatically<br/>do not attempt to bypass"]
        R2["EVIDENCE IS MANDATORY<br/>no finding valid without evidence<br/>every claim backed by tool output"]
        R3["BE METHODICAL<br/>don't skip recon<br/>don't report without confirming<br/>don't confirm without evidence"]
        R4["RECORD EVERYTHING<br/>record_finding for confirmed issues<br/>evidence chain captures automatically"]
    end

    RULES -.->|governs| RECON
    RULES -.->|governs| EXPLOIT
    RULES -.->|governs| EVID

    style RECON fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style HYP fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EXPLOIT fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style EVID fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style TRIAGE fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style REPORT fill:#0d1b2a,stroke:#e65c00,color:#e4e4e8
    style RULES fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
    style R1 fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
    style R2 fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
    style R3 fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
    style R4 fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8

Reading: The SECURITY_SYSTEM_PROMPT (harness.py, lines 66-108) encodes the offensive state machine — six phases the model works through in order. RECON maps the surface (port_scan, http_probe). HYPOTHESIS forms vulnerability theories from recon data. EXPLOIT tests those theories (http_probe with specific parameters, code_scan for static vulns). EVIDENCE formally records confirmed issues via record_finding with severity, location, CWE classification, and linked evidence. TRIAGE filters false positives through the deterministic pipeline (dedup, evidence check, known-FP, confidence threshold). REPORT produces the client deliverable via generate_report. The feedback loop (dashed line back to RECON) reflects that triage may reveal gaps requiring more recon. The four critical rules (bottom) are advisory constraints in the prompt — "scope is absolute," "evidence is mandatory," "be methodical," "record everything." The important distinction: the scope rule in the prompt is advisory; the scope enforcement in the tool executors is structural. The prompt tells the model what the methodology is; the tools enforce the boundaries. This is the division of labor between the system prompt (the brain's domain knowledge) and the harness (the structural guarantees).


Diagram 6 — Tool Execution Lifecycle

flowchart TB
    LOOP(["run_agent_loop<br/>yields ToolExecutionStartEvent"]) --> CALL

    subgraph CALL["tool.execute(arguments, signal)"]
        direction TB
        S1["1. Parse arguments<br/>host = arguments.get 'host'<br/>url = arguments.get 'url'<br/>path = arguments.get 'path'"]

        S2["2. assert_in_scope<br/>scope, target, tool_name<br/><br/>deny-rules first → any deny = out<br/>then allow-rules → ≥1 allow required<br/>then rate check → per-host counter<br/><br/>FAIL → ScopeViolation → _blocked result<br/>PASS → scope.record_request"]

        S3["3. Execute operation<br/><br/>port_scan: nmap or _simulate_scan<br/>http_probe: urllib.request.urlopen<br/>code_scan: rglob + regex patterns<br/>record_finding: Finding.create<br/>generate_report: report.generate_report"]

        S4["4. Capture evidence<br/><br/>Evidence.capture:<br/>  id = uuid4<br/>  captured_at = ISO 8601<br/>  sha256 = SHA-256(raw)<br/>chain.add_evidence:<br/>  chain_hash = SHA-256(prev + ':' + sha256)"]

        S5["5. Return AgentToolResult<br/><br/>_success:<br/>  ok=True<br/>  content = summary<br/>  data = evidence_id, evidence_hash, finding_id<br/><br/>_blocked:<br/>  ok=False<br/>  content = BLOCKED by scope<br/>  data = blocked=True, reason"]

        S1 --> S2 --> S3 --> S4 --> S5
    end

    S5 --> RESULT(["AgentToolResult<br/>appended to transcript"])
    RESULT --> LOOP_END(["run_agent_loop<br/>yields ToolExecutionEndEvent"])
    LOOP_END --> CHAIN(["EvidenceChain<br/>record already captured in step 4"])
    LOOP_END --> LISTENER(["Event listeners<br/>e.g. operator monitoring stream"])
    LOOP_END --> STEER_CHECK{steering queue<br/>has messages?}
    STEER_CHECK -->|yes| DRAIN["drain steering<br/>inject human correction<br/>next turn"]
    STEER_CHECK -->|no| NEXT(["next tool call or turn end"])

    style LOOP fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style CALL fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S1 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S2 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style S3 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S4 fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style S5 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style RESULT fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style LOOP_END fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style CHAIN fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style LISTENER fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style STEER_CHECK fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style DRAIN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style NEXT fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8

Reading: This is the complete lifecycle of a single security tool call, from the agent loop's perspective. The loop yields ToolExecutionStartEvent, then calls tool.execute(arguments, signal). Inside the executor (the boxed region), five steps run in order. Step 1 parses the structured arguments the model provided. Step 2 is the scope gate — assert_in_scope checks deny-rules, then allow-rules, then the rate limit. A failure here short-circuits to a _blocked result; the operation in step 3 never runs. Step 3 executes the actual security operation (nmap, urllib, file scan, finding creation, or report generation). Step 4 captures evidence: Evidence.capture generates a UUID, ISO timestamp, and SHA-256 content hash; chain.add_evidence computes the chain hash linking to the previous record. Step 5 returns a structured AgentToolResult_success with evidence/finding IDs attached, or _blocked with the reason. The result flows back to the loop, which appends it to the transcript, yields ToolExecutionEndEvent, and checks the steering queue for human corrections. The evidence was already captured in step 4 — the ToolExecutionEndEvent is the notification, not the capture mechanism. An operator watching the event stream sees the result; the evidence chain already has the record. The steering check at the end is the human-in-the-loop seam: if an operator called harness.steer() during this tool's execution, the steering message is drained and injected as the next turn, allowing mid-run correction without stopping the engagement.

# Diagrams — The tau-security Package

**Course**: 2A — Building AI Harnesses for Cybersecurity
**Package**: tau-security

These six diagrams accompany the teaching document (`01-teaching-document.md`). They cover tau's three-layer architecture, the transformation from coding agent to security agent, scope enforcement, evidence chaining, the offensive state machine, and the tool execution lifecycle.

---

## Diagram 1 — tau's Three-Layer Architecture

```mermaid
flowchart TB
    subgraph TAU_AI["tau_ai — provider/model streaming layer"]
        PROV[ModelProvider<br/>OpenAI · Anthropic · OpenRouter<br/>HF · Google · Mistral · local]
        PEVT[Provider events:<br/>ResponseStart · TextDelta<br/>ThinkingDelta · ResponseEnd<br/>Error · Retry]
        PROV --> PEVT
    end

    subgraph TAU_AGENT["tau_agent — the portable brain (zero UI/CLI deps)"]
        H[AgentHarness<br/>harness.py · 298 LOC<br/>owns transcript + queues + listeners]
        L[run_agent_loop<br/>loop.py · 276 LOC<br/>pure stateless provider/tool loop]
        T[AgentTool<br/>tools.py · 78 LOC<br/>frozen dataclass + async executor]
        E[14 AgentEvents<br/>events.py · 134 LOC<br/>the layer contract]
        S[Session JSONL<br/>append-only · parent_id branching<br/>tamper-evidence foundation]
        H -->|delegates| L
        L -->|emits| E
        L -->|calls| T
        L -->|appends to| S
    end

    subgraph TAU_CODING["tau_coding — the coding app"]
        CT[read · write · edit · bash<br/>glob · grep<br/>1,057 LOC of tools]
        TUI[TUI · Textual<br/>CLI · slash commands<br/>skills · on-disk sessions]
        CT --> TUI
    end

    subgraph TAU_SECURITY["tau_security — the security app (sibling)"]
        ST[port_scan · http_probe · code_scan<br/>record_finding · generate_report<br/>scope-checked executors]
        SCOPE[Scope<br/>hard-wired legal boundary<br/>assert_in_scope on every call]
        CHAIN[EvidenceChain<br/>SHA-256 hash chain<br/>tamper-evident]
        TRIAGE[TriagePipeline<br/>candidate → confirmed | rejected]
        RPT[Report<br/>JSON + HTML · integrity verified]
        ST --> SCOPE
        ST --> CHAIN
        CHAIN --> TRIAGE
        TRIAGE --> RPT
    end

    PEVT -->|provider.stream_response| L
    E -->|AgentEvent stream<br/>the contract| TUI
    E -->|AgentEvent stream<br/>same contract| ST

    style TAU_AI fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style TAU_AGENT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TAU_CODING fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style TAU_SECURITY fill:#1a0a0a,stroke:#e65c00,color:#e4e4e8
    style H fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style L fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style T fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style PROV fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style PEVT fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style CT fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style TUI fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style ST fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style SCOPE fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style CHAIN fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style TRIAGE fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style RPT fill:#1a0a0a,stroke:#e65c00,color:#e65c00
```

**Reading**: tau's load-bearing design decision is the boundary between `tau_agent` (the portable brain) and the app layers (`tau_coding`, `tau_security`). The brain depends only on `tau_ai` and Pydantic — it has zero knowledge of Textual, Rich, local config paths, slash commands, rendering, scope, or evidence. The `AgentEvent` stream is the contract: `tau_coding`'s TUI subscribes to it to render status lines; `tau_security`'s evidence chain subscribes to it to capture `ToolExecutionEndEvent` as hash-chained records. Same brain, same event stream, two completely different apps. The brain does not know which app is driving it. This is why `tau-security` is a sibling package that imports from `tau_agent` rather than a fork — it reuses the brain verbatim and swaps only the app layer. The numbers are the actual source line counts (harness.py 298, loop.py 276, tools.py 78, events.py 134). The entire brain is ~786 lines; the security layer adds ~600.

---

## Diagram 2 — The tau_coding → tau_security Transformation

```mermaid
flowchart LR
    subgraph SAME["What stays the same (the brain)"]
        BRAIN[AgentHarness<br/>same transcript ownership]
        LOOP[run_agent_loop<br/>same 276-line pure loop]
        TOOL_ABS[AgentTool abstraction<br/>same frozen dataclass]
        EVENTS[14 AgentEvents<br/>same event stream]
        STEER[steer / follow_up<br/>same HITL mechanism]
        SESSION[Session JSONL<br/>same append-only durability]
    end

    subgraph CHANGED["What changes (the harness)"]
        direction TB
        PROMPT["System prompt<br/>coding assistant<br/>↓<br/>offensive state machine<br/>Recon→Hypothesis→Exploit<br/>→Evidence→Triage→Report"]
        TOOLS["Tools<br/>read · write · edit · bash<br/>↓<br/>port_scan · http_probe · code_scan<br/>record_finding · generate_report"]
        SCOPE_CH["Scope<br/>none<br/>↓<br/>hard-wired legal boundary<br/>assert_in_scope on every call"]
        EVIDENCE_CH["Evidence<br/>session history<br/>↓<br/>tamper-evident SHA-256 hash chain"]
        OUTPUT_CH["Output<br/>code changes<br/>↓<br/>client-ready security report<br/>JSON + HTML · integrity verified"]
    end

    BRAIN -.->|unchanged| PROMPT
    LOOP -.->|unchanged| TOOLS
    TOOL_ABS -.->|unchanged| SCOPE_CH
    EVENTS -.->|unchanged| EVIDENCE_CH
    STEER -.->|unchanged| OUTPUT_CH

    style SAME fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CHANGED fill:#1a0a0a,stroke:#e65c00,color:#e4e4e8
    style BRAIN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style LOOP fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TOOL_ABS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EVENTS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style STEER fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SESSION fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style PROMPT fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style TOOLS fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style SCOPE_CH fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style EVIDENCE_CH fill:#1a0a0a,stroke:#e65c00,color:#e65c00
    style OUTPUT_CH fill:#1a0a0a,stroke:#e65c00,color:#e65c00
```

**Reading**: This diagram is the course thesis made visual. The left column is the brain — everything that stays identical when you transform a coding agent into a security agent. The `AgentHarness`, `run_agent_loop`, the `AgentTool` abstraction, the 14 event types, the steering mechanism, the session durability: none of these change. The right column is the harness — the four things that do change. The system prompt encodes the offensive state machine. The tools swap from file/shell operations to security operations. The scope goes from nonexistent to a hard-wired code-level gate. The evidence goes from a session transcript to a tamper-evident hash chain. The output goes from code changes to a client-ready report. Four changes, none touching the brain. This is what "the model is 1.6% of the system, the harness is the other 98.4%" means in practice: the brain (which includes the model invocation) is reused verbatim; all the security-specific engineering lives in the harness layer.

---

## Diagram 3 — Scope Enforcement Flow

```mermaid
flowchart TB
    START([Model calls a security tool<br/>e.g. port_scan host=10.0.0.5]) --> EXTRACT[Extract target from arguments<br/>host = arguments 'host']
    EXTRACT --> GATE{assert_in_scope<br/>scope, target, tool_name}

    GATE -->|deny-rule matches| BLOCK ScopeViolation raised
    GATE -->|no allow-rule matches| BLOCK
    GATE -->|rate limit exceeded| BLOCK

    BLOCK --> BLOCKED_RESULT[_blocked result<br/>ok=False · data: blocked=True<br/>operation NEVER executes]
    BLOCKED_RESULT --> END([Return AgentToolResult])

    GATE -->|in-scope + rate OK| RECORD[scope.record_request<br/>increment host counter]
    RECORD --> EXEC[Execute the operation<br/>nmap · urllib · file scan]
    EXEC --> CAPTURE[_capture_evidence<br/>Evidence.capture → chain.add_evidence]
    CAPTURE --> DETECT{auto-detect issues?<br/>SQL errors · missing headers<br/>500 status · vuln patterns}
    DETECT -->|issue found| FINDING[Finding.create<br/>chain.add_finding<br/>chain.link_evidence]
    DETECT -->|no issue| SUCCESS
    FINDING --> SUCCESS[_success result<br/>ok=True · data: evidence_id<br/>evidence_hash · finding_id]
    SUCCESS --> END

    style START fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style EXTRACT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style GATE fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style BLOCK fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style BLOCKED_RESULT fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style RECORD fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EXEC fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CAPTURE fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style DETECT fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style FINDING fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style SUCCESS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style END fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
```

**Reading**: This is the structural defense. Every target-touching tool call flows through this path. The model calls `port_scan` with a host argument. The executor extracts the target and calls `assert_in_scope(scope, target, tool_name)`. Three things can fail the gate: a deny-rule matches (explicit exclusion), no allow-rule matches (target was never in scope), or the rate limit is exceeded (engagement is causing impact). On any failure, a `ScopeViolation` is raised, caught by the executor, and returned as a `_blocked` result — the operation never executes. The model receives a structured result telling it the call was blocked and why. On success, the request is recorded (rate tracking), the operation executes, evidence is captured into the hash chain, and any auto-detected issues become `Finding` records linked to that evidence. The critical property: the scope check happens BEFORE the operation, in code, regardless of what the model was told to do via prompt injection. This is why `tau-security` is not vulnerable to the InjecAgent ~50% failure rate for out-of-scope actions — the injection can steer the model, but the structural gate stops the action.

---

## Diagram 4 — Evidence Chain Hash Linking

```mermaid
flowchart LR
    GENESIS(["genesis<br/>_previous_hash = 'genesis'<br/>(initial state)"])

    subgraph E1["Evidence 1"]
        E1_RAW["raw: nmap output<br/>sha256: hash(content_1)"]
        E1_CHAIN["chain_hash =<br/>SHA256('genesis' + ':' + hash_1)"]
    end

    subgraph E2["Evidence 2"]
        E2_RAW["raw: HTTP response<br/>sha256: hash(content_2)"]
        E2_CHAIN["chain_hash =<br/>SHA256(chain_hash_1 + ':' + hash_2)"]
    end

    subgraph EN["Evidence N"]
        EN_RAW["raw: code snippet<br/>sha256: hash(content_n)"]
        EN_CHAIN["chain_hash =<br/>SHA256(chain_hash_n-1 + ':' + hash_n)"]
    end

    GENESIS -->|input to| E1_CHAIN
    E1_CHAIN -->|input to| E2_CHAIN
    E2_CHAIN -->|...| EN_CHAIN
    EN_CHAIN --> HEAD(["chain_head_hash<br/>= _previous_hash<br/>(final chain_hash_n)"])

    subgraph VERIFY["verify_integrity — recomputes from genesis"]
        V1["previous = 'genesis'"]
        V2["for each record:<br/>expected = SHA256(previous + ':' + record.sha256)<br/>match? → previous = expected"]
        V3["final previous == chain_head_hash?<br/>(catches truncation)"]
        V1 --> V2 --> V3
    end

    HEAD -.->|compared against| V3

    subgraph TAMPER["Tamper scenario"]
        T1["Attacker modifies Evidence 2.raw"]
        T2["Evidence 2.sha256 no longer matches stored"]
        T3["Evidence 2.chain_hash recomputes differently"]
        T4["Evidence 3+ chain_hash all break<br/>(cascade)"]
        T5["verify_integrity → False"]
        T1 --> T2 --> T3 --> T4 --> T5
    end

    style GENESIS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E1 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E2 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E1_RAW fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E1_CHAIN fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style E2_RAW fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E2_CHAIN fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style EN_RAW fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EN_CHAIN fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style HEAD fill:#0d1b2a,stroke:#e65c00,color:#e4e4e8
    style VERIFY fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style V1 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style V2 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style V3 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TAMPER fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T1 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T2 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T3 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T4 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style T5 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
```

**Reading**: The evidence chain is append-only, and each record's hash incorporates the previous record's hash. The chain starts at `"genesis"` — a fixed initial string, not a real hash. Evidence 1's chain hash is `SHA-256("genesis:" + content_hash_1)`. Evidence 2's chain hash is `SHA-256(chain_hash_1 + ":" + content_hash_2)`. Each record depends on all previous records. The tamper scenario (bottom) shows what happens when an attacker modifies Evidence 2's `raw` field: its `sha256` no longer matches the stored value, its `chain_hash` recomputes differently, and every subsequent record's `chain_hash` breaks in a cascade. `verify_integrity()` walks the chain from genesis, recomputing each hash, and returns `False` at the first mismatch. It also checks that the final recomputed hash matches the stored `chain_head_hash` (`_previous_hash`) — this catches truncation, where evidence was appended after the recorded head. This is the property that makes the report legally defensible: the `integrity_verified` field in the JSON/HTML report is not a claim, it is a recomputable proof. A client receiving `export_evidence("evidence.json")` can re-run the verification and confirm the chain is unbroken. This is what separates a security harness from a scanner — proof, not just detection.

---

## Diagram 5 — The Offensive State Machine as System Prompt

```mermaid
flowchart LR
    RECON["1. RECON<br/>Map the target surface<br/><br/>port_scan → open ports<br/>http_probe → endpoints<br/>Build target inventory"]

    HYP["2. HYPOTHESIS<br/>Form vuln hypotheses<br/><br/>What services are exposed?<br/>What versions?<br/>What endpoints look interesting?"]

    EXPLOIT["3. EXPLOIT<br/>Test hypotheses<br/><br/>http_probe with params<br/>→ injection, auth bypass<br/>code_scan on local files<br/>→ static vulns"]

    EVID["4. EVIDENCE<br/>Document confirmed issues<br/><br/>record_finding<br/>severity · location · description<br/>confidence · CWE<br/>Every finding MUST have<br/>linked evidence"]

    TRIAGE["5. TRIAGE<br/>Filter false positives<br/><br/>candidate → confirmed | rejected<br/>dedup by title+location<br/>has_evidence check<br/>known-FP filter<br/>confidence ≥ 0.6"]

    REPORT["6. REPORT<br/>Client deliverable<br/><br/>generate_report<br/>JSON or HTML<br/>scope · methodology<br/>findings table<br/>evidence hashes<br/>triage summary"]

    RECON --> HYP --> EXPLOIT --> EVID --> TRIAGE --> REPORT

    REPORT -.->|feedback loop<br/>gaps found| RECON

    subgraph RULES["Critical rules encoded in the prompt"]
        R1["SCOPE IS ABSOLUTE<br/>out-of-scope = blocked automatically<br/>do not attempt to bypass"]
        R2["EVIDENCE IS MANDATORY<br/>no finding valid without evidence<br/>every claim backed by tool output"]
        R3["BE METHODICAL<br/>don't skip recon<br/>don't report without confirming<br/>don't confirm without evidence"]
        R4["RECORD EVERYTHING<br/>record_finding for confirmed issues<br/>evidence chain captures automatically"]
    end

    RULES -.->|governs| RECON
    RULES -.->|governs| EXPLOIT
    RULES -.->|governs| EVID

    style RECON fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style HYP fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style EXPLOIT fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style EVID fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style TRIAGE fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style REPORT fill:#0d1b2a,stroke:#e65c00,color:#e4e4e8
    style RULES fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
    style R1 fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
    style R2 fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
    style R3 fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
    style R4 fill:#1a0a0a,stroke:#cc0000,color:#e4e4e8
```

**Reading**: The `SECURITY_SYSTEM_PROMPT` (`harness.py`, lines 66-108) encodes the offensive state machine — six phases the model works through in order. RECON maps the surface (port_scan, http_probe). HYPOTHESIS forms vulnerability theories from recon data. EXPLOIT tests those theories (http_probe with specific parameters, code_scan for static vulns). EVIDENCE formally records confirmed issues via `record_finding` with severity, location, CWE classification, and linked evidence. TRIAGE filters false positives through the deterministic pipeline (dedup, evidence check, known-FP, confidence threshold). REPORT produces the client deliverable via `generate_report`. The feedback loop (dashed line back to RECON) reflects that triage may reveal gaps requiring more recon. The four critical rules (bottom) are advisory constraints in the prompt — "scope is absolute," "evidence is mandatory," "be methodical," "record everything." The important distinction: the scope rule in the prompt is advisory; the scope enforcement in the tool executors is structural. The prompt tells the model what the methodology is; the tools enforce the boundaries. This is the division of labor between the system prompt (the brain's domain knowledge) and the harness (the structural guarantees).

---

## Diagram 6 — Tool Execution Lifecycle

```mermaid
flowchart TB
    LOOP(["run_agent_loop<br/>yields ToolExecutionStartEvent"]) --> CALL

    subgraph CALL["tool.execute(arguments, signal)"]
        direction TB
        S1["1. Parse arguments<br/>host = arguments.get 'host'<br/>url = arguments.get 'url'<br/>path = arguments.get 'path'"]

        S2["2. assert_in_scope<br/>scope, target, tool_name<br/><br/>deny-rules first → any deny = out<br/>then allow-rules → ≥1 allow required<br/>then rate check → per-host counter<br/><br/>FAIL → ScopeViolation → _blocked result<br/>PASS → scope.record_request"]

        S3["3. Execute operation<br/><br/>port_scan: nmap or _simulate_scan<br/>http_probe: urllib.request.urlopen<br/>code_scan: rglob + regex patterns<br/>record_finding: Finding.create<br/>generate_report: report.generate_report"]

        S4["4. Capture evidence<br/><br/>Evidence.capture:<br/>  id = uuid4<br/>  captured_at = ISO 8601<br/>  sha256 = SHA-256(raw)<br/>chain.add_evidence:<br/>  chain_hash = SHA-256(prev + ':' + sha256)"]

        S5["5. Return AgentToolResult<br/><br/>_success:<br/>  ok=True<br/>  content = summary<br/>  data = evidence_id, evidence_hash, finding_id<br/><br/>_blocked:<br/>  ok=False<br/>  content = BLOCKED by scope<br/>  data = blocked=True, reason"]

        S1 --> S2 --> S3 --> S4 --> S5
    end

    S5 --> RESULT(["AgentToolResult<br/>appended to transcript"])
    RESULT --> LOOP_END(["run_agent_loop<br/>yields ToolExecutionEndEvent"])
    LOOP_END --> CHAIN(["EvidenceChain<br/>record already captured in step 4"])
    LOOP_END --> LISTENER(["Event listeners<br/>e.g. operator monitoring stream"])
    LOOP_END --> STEER_CHECK{steering queue<br/>has messages?}
    STEER_CHECK -->|yes| DRAIN["drain steering<br/>inject human correction<br/>next turn"]
    STEER_CHECK -->|no| NEXT(["next tool call or turn end"])

    style LOOP fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style CALL fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S1 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S2 fill:#2a0a0a,stroke:#cc0000,color:#e4e4e8
    style S3 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S4 fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style S5 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style RESULT fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style LOOP_END fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style CHAIN fill:#0d1b2a,stroke:#e65c00,color:#5eead4
    style LISTENER fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style STEER_CHECK fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style DRAIN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style NEXT fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
```

**Reading**: This is the complete lifecycle of a single security tool call, from the agent loop's perspective. The loop yields `ToolExecutionStartEvent`, then calls `tool.execute(arguments, signal)`. Inside the executor (the boxed region), five steps run in order. Step 1 parses the structured arguments the model provided. Step 2 is the scope gate — `assert_in_scope` checks deny-rules, then allow-rules, then the rate limit. A failure here short-circuits to a `_blocked` result; the operation in step 3 never runs. Step 3 executes the actual security operation (nmap, urllib, file scan, finding creation, or report generation). Step 4 captures evidence: `Evidence.capture` generates a UUID, ISO timestamp, and SHA-256 content hash; `chain.add_evidence` computes the chain hash linking to the previous record. Step 5 returns a structured `AgentToolResult` — `_success` with evidence/finding IDs attached, or `_blocked` with the reason. The result flows back to the loop, which appends it to the transcript, yields `ToolExecutionEndEvent`, and checks the steering queue for human corrections. The evidence was already captured in step 4 — the `ToolExecutionEndEvent` is the notification, not the capture mechanism. An operator watching the event stream sees the result; the evidence chain already has the record. The steering check at the end is the human-in-the-loop seam: if an operator called `harness.steer()` during this tool's execution, the steering message is drained and injected as the next turn, allowing mid-run correction without stopping the engagement.