45 minutes · The map from B1's seven surfaces to five explicit extension points in ~3,000 lines of Python
Tau is the target. Every defense in B2–B8 is a missing feature in tau today — and every one attaches at a clean extension point. This deep-dive is the map.
Deep-Dives
The three-layer split
tau_ai — provider/model streaming (provider-neutral)
tau_agent — reusable brain: harness, loop, tools, events
tau_coding — coding app: CLI/TUI, read/write/edit/bash, sessions
The load-bearing boundary
AgentHarness = reusable brain
CodingSession = coding environment
TUI = one possible frontend
Core knows nothing about rendering
| Property | What it means |
|---|---|
| It is real | A bash tool that actually runs asyncio.create_subprocess_shell. A credential store that actually holds plaintext API keys on disk. When you attack tau, you attack a real system. |
| It is small | ~3,000 LOC across three packages. A student reads the entire codebase in an afternoon and understands every line that matters for security. |
| It is undefended | None of the controls B2–B8 build exist today. The "before" picture — raw, injectable, exfiltratable. Every control you add is a net-new feature. |
tau's attack surface mapped to B-modules
| Surface | Where | Module | Tau today |
|---|---|---|---|
| 1. Agent loop | loop.py:190 | B1/B2 | no interception · executes on request |
| 2. Tool output | loop.py:260 | B2/B4 | unfiltered into transcript |
| 3. Sessions | session.py:1378 | B3 | no memory-write gate · JSONL poison persists |
| 4. Provider | tau_ai | B0 | no provider-authz check |
| 5. Credentials | credentials.py | B5 | plaintext 0600 · bash can cat/printenv |
| 6. Sandbox | tools.py:470 | B7 | zero isolation · no egress/allowlist |
| 7. Observability | events.py | B8 | events emitted · no listener analyzes |
tool.execute → tool.executor. You do not patch run_agent_loop. The architectural lesson: wrap the executor.
Where each control attaches — wrapping, replacing, subscribing
register_tool. No hook framework. No middleware. Verified by grepping the entire source. Every control attaches by wrapping or replacing objects at explicit, named sites.
| Extension point | Where | Module | Mechanism |
|---|---|---|---|
| 1 Executor wrapping | tools.py:61 | B2/B4 | replace(tool, executor=gated) |
| 2 Bash factory | tools.py:574 | B7 | replace create_bash_tool |
| 3 Credential store | provider_runtime.py:46 | B5 | vault implements CredentialReader |
| 4 Event subscription | harness.py:124 | B8 | harness.subscribe(listener) |
| 5 Tool-list construction | session.py:286 | B0/B1 | inject CodingSessionConfig.tools |
@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
def wrap_with_gate(inner: AgentTool, gate) -> AgentTool:
async def gated(arguments, signal=None):
gate.inspect(inner.name, arguments) # pre-execution
result = await inner.executor(arguments, signal=signal)
return gate.sanitize(inner.name, result) # post-execution
return replace(inner, executor=gated)
Why a tool-list wedge is bypassable — and the factory-level fix
create_bash_tool factory (tools.py:574)
│
┌────┴────┐
▼ ▼
PATH 1 PATH 2 — run_terminal_command
harness (session.py:1183)
list constructs its OWN create_bash_tool
calls it directly at session.py:1187
NEVER enters CodingSessionConfig.tools
A B7 sandbox that wraps only EP1 (tool list)
is BYPASSED by PATH 2 — it never sees it.
FIX: replace create_bash_tool ITSELF
→ both paths receive the sandbox policy
CodingSession.load. Every executor wrapped: scope gate (B0/B1), taint gate (B2), output tagged <untrusted>.
create_bash_tool replaces the factory. BOTH paths covered. Default-deny egress, allowlist, resource caps (B7).
FileCredentialStore in both sites. Credentials never plaintext on disk; agent cannot reach the store (B5).
harness.subscribe. Read-only observability (B8). Enforcement is EP1.
| Anti-pattern | Cure |
|---|---|
Patching run_agent_loop to add a gate | Wrap the executor (EP1). The loop delegates to tool.executor. |
| Wrapping only the harness bash tool | Wedge at the factory (EP2). run_terminal_command builds its own. |
Overloading shell_command_prefix as allowlist | It is a blind prepend — cannot see/reject. Wrap the executor. |
Looking for register_tool | None exists. Construct a custom CodingSessionConfig. |
| Using the event listener for enforcement | Events fire after the decision — read-only. Enforcement is EP1. |
Lab (07): clone tau, run the injection battery against the unmodified codebase using planted injection content, identify each surface's extension point, and map every finding to a B-module control. Python, type hints, no GPU.