Deep-Dive SDD-B11 — Tau: The Reference Harness to Attack and Harden

Course: 2B — Securing & Attacking Harnesses and LLMs Deep-Dive: SDD-B11 — Tau: The Reference Harness to Attack and Harden Duration: 45 minutes Level: Senior Engineer and above Prerequisites: Course 1 complete (DD-01 Pi); Course 2B Modules B1–B8

Tau is the target. Every defense in B2–B8 is a missing feature in tau today — and every one of them attaches at a clean, explicit extension point in ~3,000 lines of readable Python. This deep-dive is the map.


Learning Objectives

After completing this deep-dive, you will be able to:

  1. Explain tau's three-layer architecture (tau_ai / tau_agent / tau_coding) and why it is the ideal teaching target for harness security — small enough to read end-to-end, real enough to attack.
  2. Map tau's surfaces to the seven attack surfaces from B1, and tau's current undefended state to each B-module's control.
  3. Identify the precise extension points where each control attaches: tool-executor wrapping, credential-store replacement, bash-factory interception, and event-stream observability.
  4. Explain why tau has no plugin system — and why that makes it a better teaching target (every wedge point is explicit, no framework magic to learn).
  5. Describe the two-bash-tool finding: run_terminal_command (session.py:1183) constructs its own bash tool outside the harness tool list, bypassing any tool-layer control that doesn't wedge at the factory level.

What tau is

Tau is Hugging Face's educational reimplementation of Pi (Course 1 DD-01). Built by Alejandro AO under the Hugging Face organization, it is a "small, readable terminal coding agent — and a working example of how coding agents are built." The homepage is twotimespi.dev; the code is at github.com/huggingface/tau.

The relationship matters: Pi (DD-01) is the minimal baseline that Course 1 deep-dived as the 4-tool, ~1,200-LOC reference thin harness. Tau is Pi's educational clone — same architecture, same philosophy, but with a three-layer package split that makes the extension points cleaner to teach. If DD-01 was "here is the simplest harness that works," SDD-B11 is "here is the simplest harness we can break and fix."

The three-layer architecture

tau_ai      → provider/model streaming layer (provider-neutral)
tau_agent   → the reusable brain: harness, loop, tools, events, sessions
tau_coding  → the coding app: CLI/TUI, read/write/edit/bash tools, skills, on-disk sessions

The load-bearing boundary:

AgentHarness = reusable brain
CodingSession = coding-agent environment
TUI = one possible frontend

The core (tau_agent) does not know about Textual, Rich, local config paths, slash commands, or rendering. Frontends consume events. This separation is what makes tau a clean target: the security controls attach either in tau_agent (the loop/tools/events layer, where enforcement belongs) or in tau_coding (the filesystem/credential/session layer, where the attack surface lives).

Why tau is the ideal teaching target for 2B

Three properties:

  1. It is real. Not a stub, not a simulation. A bash tool that actually runs asyncio.create_subprocess_shell. A read tool that actually reads files. A credential store that actually holds plaintext API keys on disk. When you attack tau, you are attacking a real system.
  2. It is small. ~3,000 lines across three packages. A student can read the entire codebase in an afternoon and understand every line that matters for security.
  3. It is undefended. Tau has none of the controls that B2–B8 build. This is the "before" picture — the raw, injectable, exfiltratable harness that the course teaches you to harden. Every control you add is a net-new feature, and its absence is the vulnerability you're closing.

The "before" picture: tau's attack surface mapped to B1–B8

This is the core of the deep-dive. Each of tau's surfaces, the OWASP ASI risk it exposes, the module that covers it, and tau's current (undefended) state.

Surface 1 — The agent loop (B1, B2)

Where it lives: src/tau_agent/loop.py, function run_agent_loop.

The loop streams provider responses, extracts tool calls, executes them, and appends results to the transcript. The critical execution path is _execute_tool_calls (loop.py:190), which calls tool.execute(tool_call.arguments) for each tool the model requests.

Tau's current state: The loop has no interception point. Tool calls execute as soon as the model requests them. There is no pre-execution gate, no taint check, no scope enforcement at the loop level. The loop trusts the model's tool selection.

The defense (B2): Taint tracking and injection detection. But — critically — you do not patch the loop. The loop delegates to tool.execute, which delegates to tool.executor. The clean wedge is at the executor layer (see Extension Point 1 below), not in the loop itself. This is an important architectural lesson: enforcement belongs in the tool, not in the loop.

Surface 2 — Tool output (B2, B4)

Where it lives: src/tau_agent/loop.py, function _tool_result_message (loop.py:260).

When a tool returns an AgentToolResult, the loop converts it to a ToolResultMessage and appends it to the transcript. That message then becomes part of the context the model sees on the next turn.

Tau's current state: Tool output enters the transcript unfiltered. A file the agent reads can contain injected instructions (the indirect-injection vector from B2), and those instructions enter the model's context as if they were legitimate content. There is no <untrusted> tagging, no secondary-model injection check, no output sanitization. This is the InjecAgent ~50% vulnerability in its concrete form.

The defense (B2/B4): Tag tool output as untrusted; run it through an injection detector; wrap it in <untrusted> tags before it enters the transcript. The wedge is at the executor wrapper (Extension Point 1) — the executor returns the AgentToolResult, so it owns content before the transcript ever sees it.

Surface 3 — Memory and sessions (B3)

Where it lives: src/tau_coding/session.py (88KB — the largest file in the codebase). The session persists the transcript as append-only JSONL via SessionStorage (session.py:187). The append site is _persist_messages_since (session.py:1378).

Tau's current state: Sessions are durable and inspectable — every message, including every ToolResultMessage, is written to ~/.tau/sessions/ as JSONL. There is no memory-write gate. Nothing validates what enters a session. A poisoned tool result persists into the JSONL; when the session resumes or branches, the poison is loaded back into context. This is the sleeper attack (B3) in its concrete form: inject via a tool result in session 1, the JSONL persists it, session 2 loads it.

The defense (B3): Harness-managed writes. The harness validates what enters the session before it persists. The wedge is at _persist_messages_since (session.py:1378) — the single chokepoint where every message becomes durable — or at the executor wrapper (Extension Point 1), which sees the AgentToolResult before it's converted to a ToolResultMessage.

Surface 4 — The provider (B0)

Where it lives: src/tau_ai/ (the provider/model streaming layer) and src/tau_coding/provider_config.py (86KB — the provider configuration).

Tau's current state: The provider layer is the model API boundary. Tau supports OpenAI, Anthropic, Codex subscription auth, OpenRouter, Hugging Face, and local models. From B0's perspective, this is the "provider" link in the authorization chain — and tau has no provider-authorization check. Any tool the agent calls (including bash) can reach the provider's API endpoint. There is no scope enforcement between the agent and the provider.

The defense (B0/B1): Scope enforcement at the tool layer. The agent's tools are gated against a scope file that defines what the agent may reach. The wedge is at the tool-list construction site (session.py:286).

Surface 5 — Credentials (B5)

Where it lives: src/tau_coding/credentials.py (FileCredentialStore, stored at ~/.tau/credentials.json) and src/tau_coding/oauth.py.

Tau's current state: API keys are stored as plaintext JSON at ~/.tau/credentials.json (chmod 0600, but world-readable-by-ownership). The bash tool can run cat ~/.tau/credentials.json and the agent has the keys. It can also run printenv and leak OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENAI_CODEX_ACCESS_TOKEN from the environment. This is the Excessive Agency / Broken Access Control risk (ASI03/ASI10) in its sharpest form: the agent that can read its own credentials can exfiltrate them.

The defense (B5): Credential isolation. Credentials never appear on disk in plaintext; the agent process cannot reach the credential store directly — only the provider layer reads from it, through a vault-backed CredentialReader. The wedge is at create_model_provider (provider_runtime.py:46), the single factory that constructs the credential store.

Surface 6 — The sandbox (B7)

Where it lives: src/tau_coding/tools.py, function create_bash_tool_definition (tools.py:433). The exact execution site is the inner execute closure at tools.py:470:

process = await asyncio.create_subprocess_shell(
    shell_command, cwd=root, stdout=PIPE, stderr=STDOUT,
    start_new_session=True, executable="bash" if prefix else None,
)

Tau's current state: The bash tool runs shell commands with zero isolation. No sandbox, no network restrictions, no command allowlist, no resource caps. It has process-group kill on cancel (a reliability feature, not a security control). This is the exact "Pi has no sandbox by default" critique that surfaced on Hacker News and Reddit when Pi launched — and tau inherits it because it is Pi's educational clone.

The defense (B7): Default-deny network egress, command allowlist, resource caps. The wedge is at the create_bash_tool factory (tools.py:574) — wrapping the executor returned by create_bash_tool_definition(...).

Surface 7 — Inter-agent and session observability (B8)

Where it lives: src/tau_agent/events.py (the discriminated event union) and AgentHarness.subscribe(listener) (harness.py:124).

Tau's current state: The harness emits a rich event stream — ToolExecutionStartEvent (carries the ToolCall with name and arguments), ToolExecutionEndEvent (carries the AgentToolResult), MessageEndEvent, TurnStartEvent/TurnEndEvent. There is a subscription API (harness.subscribe(listener)). But no listener does security analysis. No session-level intent tracking, no action provenance, no anomaly detection. The event stream is for UI rendering, not attack detection.

The defense (B8): Subscribe a session-level intent tracker to the event stream. Watch ToolExecutionStartEvent for what the model is asking for, ToolExecutionEndEvent for what happened, and flag when actions drift from the session's established intent. The wedge is harness.subscribe(my_listener) — purely additive, no wrapping needed.


The extension points: where each control attaches

This is the map the capstone and the plugin pack use. Tau has no plugin system — there is no register_tool, no hook framework, no middleware. I verified this by grepping the entire source. Every control attaches by wrapping or replacing objects at explicit, named sites. This is a feature, not a limitation: it makes every wedge point auditable and teachable.

Extension Point 1 — Tool-executor wrapping (B2, B4)

AgentTool (tau_agent/tools.py:61) is a frozen dataclass:

@dataclass(frozen=True, slots=True)
class AgentTool:
    name: str
    description: str
    input_schema: Mapping[str, JSONValue]
    executor: ToolExecutor  # ← the wedge
    prompt_snippet: str | None = None
    prompt_guidelines: tuple[str, ...] = ()

ToolExecutor is a Protocol — any async callable matching the signature works:

class ToolExecutor(Protocol):
    def __call__(self, arguments: Mapping[str, JSONValue],
                 signal: ToolCancellationToken | None = None) -> Awaitable[AgentToolResult]: ...

A security-gated tool wraps the executor:

from dataclasses import replace

def wrap_with_gate(inner: AgentTool, gate) -> AgentTool:
    async def gated(arguments, signal=None):
        gate.inspect(inner.name, arguments)   # pre-execution check
        result = await inner.executor(arguments, signal=signal)
        return gate.sanitize(inner.name, result)  # post-execution sanitize
    return replace(inner, executor=gated)

This is how the taint gate (B2) and the injection detector (B2 Layer 4) attach. The wrapped tool goes into CodingSessionConfig.tools, which flows to AgentHarnessConfig(tools=...) at session.py:310. The harness passes it to the loop unchanged. No monkey-patching, no subclassing, no framework registration. A wrapping function and a dataclass replace.

Extension Point 2 — The bash tool factory (B7)

create_bash_tool (tau_coding/tools.py:574) is the factory that both bash paths go through:

  1. The harness tool list: create_coding_tools() at tools.py:96 calls create_bash_tool(cwd=root, shell_command_prefix=...).
  2. The terminal-command bar: CodingSession.run_terminal_command at session.py:1183 constructs its own create_bash_tool(cwd=self.cwd, shell_command_prefix=self._config.shell_command_prefix) and calls it directly at session.py:1187outside the harness tool list.

This second path is the two-bash-tool finding — a real bypass. A B7 control that only wraps tools in the harness list (Extension Point 1) will be bypassed by the terminal-command path. The clean fix is to wedge at the factory level: provide a hardened create_bash_tool replacement that both call sites use. The wedge point is the factory itself, not the individual tool instances.

The sandbox wraps the executor returned by create_bash_tool_definition(...):

def create_hardened_bash_tool(*, cwd, shell_command_prefix=None, policy):
    definition = create_bash_tool_definition(cwd=cwd, shell_command_prefix=shell_command_prefix)
    inner_executor = definition.executor
    async def gated(arguments, signal=None):
        command = arguments.get("command", "")
        decision = policy.check(command)   # allowlist / denylist / egress gate
        if decision.denied:
            return AgentToolResult(tool_call_id="", name="bash", ok=False,
                                   content=f"Blocked: {decision.reason}", error=decision.reason)
        return await inner_executor(arguments, signal=signal)
    return replace(definition.to_agent_tool(), executor=gated)

The shell_command_prefix mechanism (_prefixed_shell_command, tools.py:586) is NOT a viable allowlist hook — it is a blind prepend (f"{prefix}\n{command}"), evaluated as a shell preamble. It cannot see or reject the command. Do not try to overload it.

Extension Point 3 — Credential store replacement (B5)

create_model_provider (tau_coding/provider_runtime.py:46) is the single factory that constructs the credential store:

def create_model_provider(provider, *, credential_store=None, ...):
    credentials = credential_store or FileCredentialStore()  # ← the wedge (line 56)
    ...
    credential_reader=credentials  # passed into the provider config (lines 61, 89)

CredentialReader is a protocol (provider_config.py:52-55) with a single method:

class CredentialReader(Protocol):
    def get(self, name: str) -> str | None: ...

A vault replaces FileCredentialStore by implementing this protocol (plus the OAuth methods get_oauth/set_oauth used by the Codex resolver). The vault is injected via create_model_provider(credential_store=my_vault). A second FileCredentialStore instance is created in CodingSession.__init__ at session.py:253-255 — both must be replaced for full coverage.

The plaintext file at credentials_path() (~/.tau/credentials.json) and the credential env vars (OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENAI_CODEX_ACCESS_TOKEN) are the exfiltration channels the vault secures at rest — but blocking the agent from reading them is a B7 concern (the bash denylist denies cat ~/.tau/credentials.json and printenv). B5 and B7 are coupled: B5 secures credentials at rest and at the provider boundary; B7 blocks the exfiltration channel.

Extension Point 4 — Event-stream subscription (B8)

AgentHarness.subscribe(listener) (harness.py:124) registers an EventListener:

EventListener = Callable[[AgentEvent], Awaitable[None] | None]

The listener receives every event the harness emits. For B8 observability:

def attach_intent_tracker(harness: AgentHarness) -> Callable[[], None]:
    tracker = SessionIntentTracker()
    async def listener(event: AgentEvent):
        if isinstance(event, ToolExecutionStartEvent):
            tracker.observe_request(event.tool_call.name, event.tool_call.arguments)
        elif isinstance(event, ToolExecutionEndEvent):
            tracker.observe_result(event.result)
        if tracker.drift_detected():
            # flag for review / halt / alert
            ...
    return harness.subscribe(listener)

This is purely additive — no wrapping, no replacement. The listener cannot block a tool call (events fire after the decision in _execute_tool_calls), so events are for observability (B8), not enforcement (B2/B7). Enforcement belongs in the executor wrapper (Extension Point 1).

Extension Point 5 — The tool-list construction site (B0/B1 scope enforcement)

CodingSession.load at session.py:286-293 is where the tool list is built:

config.tools if config.tools is not None else create_coding_tools(
    cwd=config.cwd, shell_command_prefix=config.shell_command_prefix
)

If CodingSessionConfig.tools is provided, it short-circuits create_coding_tools. This is the single place to inject hardened tools — pass a list where every tool's executor is wrapped with the scope gate (B0/B1), the taint gate (B2), and (for bash) the sandbox policy (B7). The hardened list flows to AgentHarnessConfig(tools=...) at session.py:310 and persists across reloads.


The hardened session: putting it together

The plugin pack (tau-plugins/) assembles all four extension points into a single entry point:

from tau_plugins.hardened_session import create_hardened_session

session = create_hardened_session(cwd="/my/project")
# This session has:
# - Every tool wrapped with the taint gate (B2)
# - Bash wrapped with the sandbox policy (B7)
# - Credentials read from the vault, not plaintext (B5)
# - The intent tracker subscribed to the event stream (B8)
# - The scope gate on every outbound action (B0/B1)

The scorecard harness measures the before/after: run the InjecAgent-style injection battery against unmodified tau, then against the hardened session. The delta is the defense scorecard — the same artifact Capstone B1 ships.


Anti-Patterns

Patching the loop instead of wrapping the executor

Trying to add a pre-execution gate by modifying run_agent_loop or _execute_tool_calls. Cure: the loop delegates to tool.executetool.executor. Wrap the executor (Extension Point 1). The loop stays untouched. This is the architectural lesson: enforcement belongs in the tool, not in the loop.

Wrapping only the harness bash tool

Adding a sandbox to the bash tool in the harness list but missing run_terminal_command (session.py:1183), which constructs its own bash tool outside the list. Cure: wedge at the factory level — provide a hardened create_bash_tool replacement that both call sites use.

Overloading shell_command_prefix as an allowlist

Trying to use the command-prefix mechanism to enforce a security policy. It is a blind prepend (f"{prefix}\n{command}") — it cannot see or reject the command. Cure: wrap the executor (Extension Point 2). The prefix is for setup (env vars, sourcing), not security.

Trying to register a plugin

Looking for a register_tool or plugin-discovery hook. Tau has none. Cure: construct a custom CodingSessionConfig with wrapped tools + vault-backed provider (programmatic), or monkey-patch the factories (for the CLI). The entry point is CodingSession.load(CodingSessionConfig(...)) at session.py:259.

Using the event listener for enforcement

Subscribing a listener that tries to block a tool call. The listener fires after the decision in _execute_tool_calls — it is read-only. Cure: events are for observability (B8); enforcement belongs in the executor wrapper (Extension Point 1).


Key Terms

Term Definition
tau Hugging Face's educational reimplementation of Pi; the reference harness Course 2B attacks and hardens
The three-layer split tau_ai (provider) / tau_agent (reusable brain) / tau_coding (coding app); the boundary that makes extension points clean
Extension Point 1 Tool-executor wrapping via dataclasses.replace(AgentTool, executor=gated); where B2's taint gate attaches
Extension Point 2 The create_bash_tool factory (tools.py:574); where B7's sandbox wedges, covering both bash paths
The two-bash-tool finding run_terminal_command (session.py:1183) constructs its own bash tool outside the harness list; a factory-level wedge is required to cover both
Extension Point 3 create_model_provider (provider_runtime.py:46); where B5's vault replaces FileCredentialStore
Extension Point 4 harness.subscribe(listener) (harness.py:124); where B8's intent tracker attaches (additive, read-only)
Extension Point 5 CodingSession.load tool-list construction (session.py:286); where B0/B1 scope-gated tools are injected
CredentialReader protocol provider_config.py:52-55; the single-method interface (`get(name) -> str
No plugin system Tau has no register_tool/hook/middleware; all controls attach by wrapping or replacing at explicit named sites

Lab Exercise

See 07-lab-spec.md — "Map and Attack tau's Surfaces." Students clone tau, run the injection battery against the unmodified codebase, identify each surface's extension point, and map every finding to a B-module control. This lab is the prerequisite for Capstone B1 (harden tau) and Capstone B2 (red-team tau).


References

  1. Tau — github.com/huggingface/tau. The reference harness. MIT license. ~3,000 LOC across tau_ai / tau_agent / tau_coding.
  2. twotimespi.dev — Tau's documentation site. Architecture, agent loop, CLI reference.
  3. Pi (Course 1 DD-01) — The minimal baseline tau reimplements. 4 tools, <1k prompt, ~1,200 LOC.
  4. Course 2B Module B1 — Threat Model of Agentic Systems. The seven surfaces this deep-dive maps tau onto.
  5. Course 2B Module B2 — Prompt Injection Defense Engineering. The taint gate and injection detector (Extension Point 1).
  6. Course 2B Module B5 — Identity and Permission Design. The credential vault (Extension Point 3).
  7. Course 2B Module B7 — Sandboxes and Execution Controls. The bash sandbox and egress gate (Extension Point 2).
  8. Course 2B Module B8 — Observability and Attack Detection. The session-level intent tracker (Extension Point 4).
  9. Hacker News (Pi launch) — "Pi.dev coding agent has no sandbox by default." The critique tau inherits; the motivation for B7.
  10. Hugging Face Secure Code Execution docs — The two sandboxing approaches (containers vs. restricted interpreters) that inform the B7 plugin design.
  11. Capstone B1 — Harden tau. The defensive capstone that installs the plugins from this deep-dive's extension-point map.
  12. Capstone B2 — Red-team a tau deployment. The offensive capstone that attacks a partially-hardened tau instance.
# Deep-Dive SDD-B11 — Tau: The Reference Harness to Attack and Harden

**Course**: 2B — Securing & Attacking Harnesses and LLMs
**Deep-Dive**: SDD-B11 — Tau: The Reference Harness to Attack and Harden
**Duration**: 45 minutes
**Level**: Senior Engineer and above
**Prerequisites**: Course 1 complete (DD-01 Pi); Course 2B Modules B1–B8

> *Tau is the target. Every defense in B2–B8 is a missing feature in tau today — and every one of them attaches at a clean, explicit extension point in ~3,000 lines of readable Python. This deep-dive is the map.*

---

## Learning Objectives

After completing this deep-dive, you will be able to:

1. Explain tau's three-layer architecture (`tau_ai` / `tau_agent` / `tau_coding`) and why it is the ideal teaching target for harness security — small enough to read end-to-end, real enough to attack.
2. Map tau's surfaces to the seven attack surfaces from B1, and tau's *current undefended state* to each B-module's control.
3. Identify the precise extension points where each control attaches: tool-executor wrapping, credential-store replacement, bash-factory interception, and event-stream observability.
4. Explain why tau has no plugin system — and why that makes it a *better* teaching target (every wedge point is explicit, no framework magic to learn).
5. Describe the two-bash-tool finding: `run_terminal_command` (`session.py:1183`) constructs its own bash tool outside the harness tool list, bypassing any tool-layer control that doesn't wedge at the factory level.

---

## What tau is

Tau is Hugging Face's educational reimplementation of Pi (Course 1 DD-01). Built by Alejandro AO under the Hugging Face organization, it is a "small, readable terminal coding agent — and a working example of how coding agents are built." The homepage is twotimespi.dev; the code is at github.com/huggingface/tau.

The relationship matters: Pi (DD-01) is the minimal baseline that Course 1 deep-dived as the 4-tool, ~1,200-LOC reference thin harness. Tau is Pi's educational clone — same architecture, same philosophy, but with a three-layer package split that makes the extension points cleaner to teach. If DD-01 was "here is the simplest harness that works," SDD-B11 is "here is the simplest harness we can break and fix."

### The three-layer architecture

```
tau_ai      → provider/model streaming layer (provider-neutral)
tau_agent   → the reusable brain: harness, loop, tools, events, sessions
tau_coding  → the coding app: CLI/TUI, read/write/edit/bash tools, skills, on-disk sessions
```

The load-bearing boundary:

```
AgentHarness = reusable brain
CodingSession = coding-agent environment
TUI = one possible frontend
```

The core (`tau_agent`) does not know about Textual, Rich, local config paths, slash commands, or rendering. Frontends consume events. This separation is what makes tau a clean target: the security controls attach either in `tau_agent` (the loop/tools/events layer, where enforcement belongs) or in `tau_coding` (the filesystem/credential/session layer, where the attack surface lives).

### Why tau is the ideal teaching target for 2B

Three properties:

1. **It is real.** Not a stub, not a simulation. A `bash` tool that actually runs `asyncio.create_subprocess_shell`. A `read` tool that actually reads files. A credential store that actually holds plaintext API keys on disk. When you attack tau, you are attacking a real system.
2. **It is small.** ~3,000 lines across three packages. A student can read the entire codebase in an afternoon and understand every line that matters for security.
3. **It is undefended.** Tau has none of the controls that B2–B8 build. This is the "before" picture — the raw, injectable, exfiltratable harness that the course teaches you to harden. Every control you add is a *net-new feature*, and its absence is the vulnerability you're closing.

---

## The "before" picture: tau's attack surface mapped to B1–B8

This is the core of the deep-dive. Each of tau's surfaces, the OWASP ASI risk it exposes, the module that covers it, and tau's *current* (undefended) state.

### Surface 1 — The agent loop (B1, B2)

**Where it lives**: `src/tau_agent/loop.py`, function `run_agent_loop`.

The loop streams provider responses, extracts tool calls, executes them, and appends results to the transcript. The critical execution path is `_execute_tool_calls` (`loop.py:190`), which calls `tool.execute(tool_call.arguments)` for each tool the model requests.

**Tau's current state**: The loop has **no interception point**. Tool calls execute as soon as the model requests them. There is no pre-execution gate, no taint check, no scope enforcement at the loop level. The loop trusts the model's tool selection.

**The defense (B2)**: Taint tracking and injection detection. But — critically — **you do not patch the loop**. The loop delegates to `tool.execute`, which delegates to `tool.executor`. The clean wedge is at the executor layer (see Extension Point 1 below), not in the loop itself. This is an important architectural lesson: enforcement belongs in the tool, not in the loop.

### Surface 2 — Tool output (B2, B4)

**Where it lives**: `src/tau_agent/loop.py`, function `_tool_result_message` (`loop.py:260`).

When a tool returns an `AgentToolResult`, the loop converts it to a `ToolResultMessage` and appends it to the transcript. That message then becomes part of the context the model sees on the next turn.

**Tau's current state**: Tool output enters the transcript **unfiltered**. A file the agent `read`s can contain injected instructions (the indirect-injection vector from B2), and those instructions enter the model's context as if they were legitimate content. There is no `<untrusted>` tagging, no secondary-model injection check, no output sanitization. This is the InjecAgent ~50% vulnerability in its concrete form.

**The defense (B2/B4)**: Tag tool output as untrusted; run it through an injection detector; wrap it in `<untrusted>` tags before it enters the transcript. The wedge is at the executor wrapper (Extension Point 1) — the executor returns the `AgentToolResult`, so it owns `content` before the transcript ever sees it.

### Surface 3 — Memory and sessions (B3)

**Where it lives**: `src/tau_coding/session.py` (88KB — the largest file in the codebase). The session persists the transcript as append-only JSONL via `SessionStorage` (`session.py:187`). The append site is `_persist_messages_since` (`session.py:1378`).

**Tau's current state**: Sessions are durable and inspectable — every message, including every `ToolResultMessage`, is written to `~/.tau/sessions/` as JSONL. There is **no memory-write gate**. Nothing validates what enters a session. A poisoned tool result persists into the JSONL; when the session resumes or branches, the poison is loaded back into context. This is the sleeper attack (B3) in its concrete form: inject via a tool result in session 1, the JSONL persists it, session 2 loads it.

**The defense (B3)**: Harness-managed writes. The harness validates what enters the session before it persists. The wedge is at `_persist_messages_since` (`session.py:1378`) — the single chokepoint where every message becomes durable — or at the executor wrapper (Extension Point 1), which sees the `AgentToolResult` before it's converted to a `ToolResultMessage`.

### Surface 4 — The provider (B0)

**Where it lives**: `src/tau_ai/` (the provider/model streaming layer) and `src/tau_coding/provider_config.py` (86KB — the provider configuration).

**Tau's current state**: The provider layer is the model API boundary. Tau supports OpenAI, Anthropic, Codex subscription auth, OpenRouter, Hugging Face, and local models. From B0's perspective, this is the "provider" link in the authorization chain — and tau has **no provider-authorization check**. Any tool the agent calls (including `bash`) can reach the provider's API endpoint. There is no scope enforcement between the agent and the provider.

**The defense (B0/B1)**: Scope enforcement at the tool layer. The agent's tools are gated against a scope file that defines what the agent may reach. The wedge is at the tool-list construction site (`session.py:286`).

### Surface 5 — Credentials (B5)

**Where it lives**: `src/tau_coding/credentials.py` (`FileCredentialStore`, stored at `~/.tau/credentials.json`) and `src/tau_coding/oauth.py`.

**Tau's current state**: API keys are stored as **plaintext JSON** at `~/.tau/credentials.json` (chmod 0600, but world-readable-by-ownership). The `bash` tool can run `cat ~/.tau/credentials.json` and the agent has the keys. It can also run `printenv` and leak `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or `OPENAI_CODEX_ACCESS_TOKEN` from the environment. This is the Excessive Agency / Broken Access Control risk (ASI03/ASI10) in its sharpest form: the agent that can read its own credentials can exfiltrate them.

**The defense (B5)**: Credential isolation. Credentials never appear on disk in plaintext; the agent process cannot reach the credential store directly — only the provider layer reads from it, through a vault-backed `CredentialReader`. The wedge is at `create_model_provider` (`provider_runtime.py:46`), the single factory that constructs the credential store.

### Surface 6 — The sandbox (B7)

**Where it lives**: `src/tau_coding/tools.py`, function `create_bash_tool_definition` (`tools.py:433`). The exact execution site is the inner `execute` closure at `tools.py:470`:

```python
process = await asyncio.create_subprocess_shell(
    shell_command, cwd=root, stdout=PIPE, stderr=STDOUT,
    start_new_session=True, executable="bash" if prefix else None,
)
```

**Tau's current state**: The `bash` tool runs shell commands with **zero isolation**. No sandbox, no network restrictions, no command allowlist, no resource caps. It has process-group kill on cancel (a reliability feature, not a security control). This is the exact "Pi has no sandbox by default" critique that surfaced on Hacker News and Reddit when Pi launched — and tau inherits it because it is Pi's educational clone.

**The defense (B7)**: Default-deny network egress, command allowlist, resource caps. The wedge is at the `create_bash_tool` factory (`tools.py:574`) — wrapping the executor returned by `create_bash_tool_definition(...)`.

### Surface 7 — Inter-agent and session observability (B8)

**Where it lives**: `src/tau_agent/events.py` (the discriminated event union) and `AgentHarness.subscribe(listener)` (`harness.py:124`).

**Tau's current state**: The harness emits a rich event stream — `ToolExecutionStartEvent` (carries the `ToolCall` with `name` and `arguments`), `ToolExecutionEndEvent` (carries the `AgentToolResult`), `MessageEndEvent`, `TurnStartEvent`/`TurnEndEvent`. There is a subscription API (`harness.subscribe(listener)`). But **no listener does security analysis**. No session-level intent tracking, no action provenance, no anomaly detection. The event stream is for UI rendering, not attack detection.

**The defense (B8)**: Subscribe a session-level intent tracker to the event stream. Watch `ToolExecutionStartEvent` for what the model is asking for, `ToolExecutionEndEvent` for what happened, and flag when actions drift from the session's established intent. The wedge is `harness.subscribe(my_listener)` — purely additive, no wrapping needed.

---

## The extension points: where each control attaches

This is the map the capstone and the plugin pack use. Tau has **no plugin system** — there is no `register_tool`, no hook framework, no middleware. I verified this by grepping the entire source. Every control attaches by *wrapping* or *replacing* objects at explicit, named sites. This is a feature, not a limitation: it makes every wedge point auditable and teachable.

### Extension Point 1 — Tool-executor wrapping (B2, B4)

`AgentTool` (`tau_agent/tools.py:61`) is a frozen dataclass:

```python
@dataclass(frozen=True, slots=True)
class AgentTool:
    name: str
    description: str
    input_schema: Mapping[str, JSONValue]
    executor: ToolExecutor  # ← the wedge
    prompt_snippet: str | None = None
    prompt_guidelines: tuple[str, ...] = ()
```

`ToolExecutor` is a `Protocol` — any async callable matching the signature works:

```python
class ToolExecutor(Protocol):
    def __call__(self, arguments: Mapping[str, JSONValue],
                 signal: ToolCancellationToken | None = None) -> Awaitable[AgentToolResult]: ...
```

A security-gated tool wraps the executor:

```python
from dataclasses import replace

def wrap_with_gate(inner: AgentTool, gate) -> AgentTool:
    async def gated(arguments, signal=None):
        gate.inspect(inner.name, arguments)   # pre-execution check
        result = await inner.executor(arguments, signal=signal)
        return gate.sanitize(inner.name, result)  # post-execution sanitize
    return replace(inner, executor=gated)
```

This is how the taint gate (B2) and the injection detector (B2 Layer 4) attach. The wrapped tool goes into `CodingSessionConfig.tools`, which flows to `AgentHarnessConfig(tools=...)` at `session.py:310`. The harness passes it to the loop unchanged. **No monkey-patching, no subclassing, no framework registration.** A wrapping function and a dataclass replace.

### Extension Point 2 — The bash tool factory (B7)

`create_bash_tool` (`tau_coding/tools.py:574`) is the factory that both bash paths go through:

1. The harness tool list: `create_coding_tools()` at `tools.py:96` calls `create_bash_tool(cwd=root, shell_command_prefix=...)`.
2. The terminal-command bar: `CodingSession.run_terminal_command` at `session.py:1183` constructs its own `create_bash_tool(cwd=self.cwd, shell_command_prefix=self._config.shell_command_prefix)` and calls it directly at `session.py:1187` — **outside the harness tool list.**

This second path is the **two-bash-tool finding** — a real bypass. A B7 control that only wraps tools in the harness list (Extension Point 1) will be bypassed by the terminal-command path. The clean fix is to wedge at the factory level: provide a hardened `create_bash_tool` replacement that both call sites use. The wedge point is the factory itself, not the individual tool instances.

The sandbox wraps the executor returned by `create_bash_tool_definition(...)`:

```python
def create_hardened_bash_tool(*, cwd, shell_command_prefix=None, policy):
    definition = create_bash_tool_definition(cwd=cwd, shell_command_prefix=shell_command_prefix)
    inner_executor = definition.executor
    async def gated(arguments, signal=None):
        command = arguments.get("command", "")
        decision = policy.check(command)   # allowlist / denylist / egress gate
        if decision.denied:
            return AgentToolResult(tool_call_id="", name="bash", ok=False,
                                   content=f"Blocked: {decision.reason}", error=decision.reason)
        return await inner_executor(arguments, signal=signal)
    return replace(definition.to_agent_tool(), executor=gated)
```

The `shell_command_prefix` mechanism (`_prefixed_shell_command`, `tools.py:586`) is NOT a viable allowlist hook — it is a blind prepend (`f"{prefix}\n{command}"`), evaluated as a shell preamble. It cannot see or reject the command. Do not try to overload it.

### Extension Point 3 — Credential store replacement (B5)

`create_model_provider` (`tau_coding/provider_runtime.py:46`) is the single factory that constructs the credential store:

```python
def create_model_provider(provider, *, credential_store=None, ...):
    credentials = credential_store or FileCredentialStore()  # ← the wedge (line 56)
    ...
    credential_reader=credentials  # passed into the provider config (lines 61, 89)
```

`CredentialReader` is a protocol (`provider_config.py:52-55`) with a single method:

```python
class CredentialReader(Protocol):
    def get(self, name: str) -> str | None: ...
```

A vault replaces `FileCredentialStore` by implementing this protocol (plus the OAuth methods `get_oauth`/`set_oauth` used by the Codex resolver). The vault is injected via `create_model_provider(credential_store=my_vault)`. A second `FileCredentialStore` instance is created in `CodingSession.__init__` at `session.py:253-255` — both must be replaced for full coverage.

The plaintext file at `credentials_path()` (`~/.tau/credentials.json`) and the credential env vars (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_CODEX_ACCESS_TOKEN`) are the exfiltration channels the vault secures at rest — but blocking the *agent* from reading them is a B7 concern (the bash denylist denies `cat ~/.tau/credentials.json` and `printenv`). B5 and B7 are coupled: B5 secures credentials at rest and at the provider boundary; B7 blocks the exfiltration channel.

### Extension Point 4 — Event-stream subscription (B8)

`AgentHarness.subscribe(listener)` (`harness.py:124`) registers an `EventListener`:

```python
EventListener = Callable[[AgentEvent], Awaitable[None] | None]
```

The listener receives every event the harness emits. For B8 observability:

```python
def attach_intent_tracker(harness: AgentHarness) -> Callable[[], None]:
    tracker = SessionIntentTracker()
    async def listener(event: AgentEvent):
        if isinstance(event, ToolExecutionStartEvent):
            tracker.observe_request(event.tool_call.name, event.tool_call.arguments)
        elif isinstance(event, ToolExecutionEndEvent):
            tracker.observe_result(event.result)
        if tracker.drift_detected():
            # flag for review / halt / alert
            ...
    return harness.subscribe(listener)
```

This is purely additive — no wrapping, no replacement. The listener cannot block a tool call (events fire after the decision in `_execute_tool_calls`), so events are for **observability** (B8), not **enforcement** (B2/B7). Enforcement belongs in the executor wrapper (Extension Point 1).

### Extension Point 5 — The tool-list construction site (B0/B1 scope enforcement)

`CodingSession.load` at `session.py:286-293` is where the tool list is built:

```python
config.tools if config.tools is not None else create_coding_tools(
    cwd=config.cwd, shell_command_prefix=config.shell_command_prefix
)
```

If `CodingSessionConfig.tools` is provided, it short-circuits `create_coding_tools`. This is the single place to inject hardened tools — pass a list where every tool's executor is wrapped with the scope gate (B0/B1), the taint gate (B2), and (for bash) the sandbox policy (B7). The hardened list flows to `AgentHarnessConfig(tools=...)` at `session.py:310` and persists across reloads.

---

## The hardened session: putting it together

The plugin pack (`tau-plugins/`) assembles all four extension points into a single entry point:

```python
from tau_plugins.hardened_session import create_hardened_session

session = create_hardened_session(cwd="/my/project")
# This session has:
# - Every tool wrapped with the taint gate (B2)
# - Bash wrapped with the sandbox policy (B7)
# - Credentials read from the vault, not plaintext (B5)
# - The intent tracker subscribed to the event stream (B8)
# - The scope gate on every outbound action (B0/B1)
```

The scorecard harness measures the before/after: run the InjecAgent-style injection battery against unmodified tau, then against the hardened session. The delta is the defense scorecard — the same artifact Capstone B1 ships.

---

## Anti-Patterns

### Patching the loop instead of wrapping the executor
Trying to add a pre-execution gate by modifying `run_agent_loop` or `_execute_tool_calls`. Cure: the loop delegates to `tool.execute` → `tool.executor`. Wrap the executor (Extension Point 1). The loop stays untouched. This is the architectural lesson: enforcement belongs in the tool, not in the loop.

### Wrapping only the harness bash tool
Adding a sandbox to the bash tool in the harness list but missing `run_terminal_command` (`session.py:1183`), which constructs its own bash tool outside the list. Cure: wedge at the factory level — provide a hardened `create_bash_tool` replacement that both call sites use.

### Overloading `shell_command_prefix` as an allowlist
Trying to use the command-prefix mechanism to enforce a security policy. It is a blind prepend (`f"{prefix}\n{command}"`) — it cannot see or reject the command. Cure: wrap the executor (Extension Point 2). The prefix is for setup (env vars, sourcing), not security.

### Trying to register a plugin
Looking for a `register_tool` or plugin-discovery hook. Tau has none. Cure: construct a custom `CodingSessionConfig` with wrapped `tools` + vault-backed provider (programmatic), or monkey-patch the factories (for the CLI). The entry point is `CodingSession.load(CodingSessionConfig(...))` at `session.py:259`.

### Using the event listener for enforcement
Subscribing a listener that tries to block a tool call. The listener fires after the decision in `_execute_tool_calls` — it is read-only. Cure: events are for observability (B8); enforcement belongs in the executor wrapper (Extension Point 1).

---

## Key Terms

| Term | Definition |
| --- | --- |
| **tau** | Hugging Face's educational reimplementation of Pi; the reference harness Course 2B attacks and hardens |
| **The three-layer split** | `tau_ai` (provider) / `tau_agent` (reusable brain) / `tau_coding` (coding app); the boundary that makes extension points clean |
| **Extension Point 1** | Tool-executor wrapping via `dataclasses.replace(AgentTool, executor=gated)`; where B2's taint gate attaches |
| **Extension Point 2** | The `create_bash_tool` factory (`tools.py:574`); where B7's sandbox wedges, covering both bash paths |
| **The two-bash-tool finding** | `run_terminal_command` (`session.py:1183`) constructs its own bash tool outside the harness list; a factory-level wedge is required to cover both |
| **Extension Point 3** | `create_model_provider` (`provider_runtime.py:46`); where B5's vault replaces `FileCredentialStore` |
| **Extension Point 4** | `harness.subscribe(listener)` (`harness.py:124`); where B8's intent tracker attaches (additive, read-only) |
| **Extension Point 5** | `CodingSession.load` tool-list construction (`session.py:286`); where B0/B1 scope-gated tools are injected |
| **CredentialReader protocol** | `provider_config.py:52-55`; the single-method interface (`get(name) -> str | None`) the vault implements |
| **No plugin system** | Tau has no `register_tool`/hook/middleware; all controls attach by wrapping or replacing at explicit named sites |

---

## Lab Exercise

See `07-lab-spec.md` — "Map and Attack tau's Surfaces." Students clone tau, run the injection battery against the unmodified codebase, identify each surface's extension point, and map every finding to a B-module control. This lab is the prerequisite for Capstone B1 (harden tau) and Capstone B2 (red-team tau).

---

## References

1. **Tau** — github.com/huggingface/tau. The reference harness. MIT license. ~3,000 LOC across `tau_ai` / `tau_agent` / `tau_coding`.
2. **twotimespi.dev** — Tau's documentation site. Architecture, agent loop, CLI reference.
3. **Pi** (Course 1 DD-01) — The minimal baseline tau reimplements. 4 tools, <1k prompt, ~1,200 LOC.
4. **Course 2B Module B1** — Threat Model of Agentic Systems. The seven surfaces this deep-dive maps tau onto.
5. **Course 2B Module B2** — Prompt Injection Defense Engineering. The taint gate and injection detector (Extension Point 1).
6. **Course 2B Module B5** — Identity and Permission Design. The credential vault (Extension Point 3).
7. **Course 2B Module B7** — Sandboxes and Execution Controls. The bash sandbox and egress gate (Extension Point 2).
8. **Course 2B Module B8** — Observability and Attack Detection. The session-level intent tracker (Extension Point 4).
9. **Hacker News (Pi launch)** — "Pi.dev coding agent has no sandbox by default." The critique tau inherits; the motivation for B7.
10. **Hugging Face Secure Code Execution docs** — The two sandboxing approaches (containers vs. restricted interpreters) that inform the B7 plugin design.
11. **Capstone B1** — Harden tau. The defensive capstone that installs the plugins from this deep-dive's extension-point map.
12. **Capstone B2** — Red-team a tau deployment. The offensive capstone that attacks a partially-hardened tau instance.