Harden the tau Harness

Capstone B1 · Course 2B — Securing & Attacking Harnesses and LLMs

120 minutes · Take the real tau codebase, measure its baseline, install three tested plugins, ship a defense scorecard

The old version built a stub harness against a fake model. This version attacks real tau — Hugging Face's educational coding agent — runs the injection battery, installs the plugins, and produces a measured before/after scorecard. Real fork. Measured hardening. Not a simulation.

Capstones

The capstone goal

Clone real tau, measure its baseline vulnerability, install three tested security plugins (tau_taint, tau_sandbox, tau_vault), and ship a defense scorecard showing the measured delta.

The fork

A real tau install with the three plugins attached at SDD-B11's five extension points — taint gate (B2), bash sandbox (B7), credential vault (B5), plus observability wiring (B0/B1/B8).

The proof

An InjecAgent-style battery run against unmodified tau (baseline) and against the hardened fork. The scorecard shows the curve — ~33% → ~0% on the deterministic-gate classes.

Success criterion: a real tau fork and a measured scorecard. Both ship together. The plugin pack is tested — 31/31 tests pass against real tau. The scorecard reproduces the measured delta.

Why the rewrite — real harness beats stub

The old capstone built a stub in TypeScript with a fake model. It taught the architecture. It could not teach the integration — and integration is what breaks in production.
Only a real harness surfacesConcrete example in tau
Integration hazardsThe two-bash-tool finding — run_terminal_command (session.py:1183) builds its own bash outside the harness list. A stub has no second path to find.
A real baselinecat ~/.tau/credentials.json actually returns your API key. The 33% is vulnerability, not a simulation of it.
Wedge-point disciplineFive named extension points at real line numbers. Not abstractions — actual attachment sites you grep for.

SDD-B11 mapped tau's surfaces and named the extension points. This capstone is the build that follows the map.

tau's seven surfaces & the five extension points

SEVEN SURFACES (SDD-B11)              FIVE EXTENSION POINTS
1. agent loop      (loop.py)        EP1 tool-executor wrap    — tau_taint (B2)
2. tool output     (loop.py)        EP2 bash-tool factory     — tau_sandbox (B7)
3. memory/sessions (session.py)     EP3 credential store      — tau_vault (B5)
4. provider        (tau_ai)         EP4 event subscribe       — B8 intent
5. credentials     (credentials.py) EP5 tool-list site        — composes all
6. sandbox/bash    (tools.py)
7. inter-agent     (events.py)

This capstone installs 3 of the 7 defenses (B2, B5, B7).
The other 4 are NAMED RESIDUALS in the honest scorecard.
tau has no plugin system — no register_tool, no hooks, no middleware. Every control attaches by wrapping or replacing at a named site. That is what makes the wedge points auditable.

The six phases — install, re-run, attribute

Each phase installs one layer against real tau and re-runs the battery

Phase 1 — Clone and baseline tau

PHASE 1 · git clone · pip install · run the battery on unmodified tau
git clone https://github.com/huggingface/tau
cd tau && pip install -e .
python -c "from tau_coding.tools import create_coding_tools; print('ok')"

# Run the baseline battery
from tau_plugins.scorecard import run_battery, plant_injection_files
from tau_coding.tools import create_coding_tools
plant_injection_files()
baseline = await run_battery(create_coding_tools(cwd="/tmp"), label="baseline")
print(baseline.to_table())
The baseline is real. tau actually executes rm -rf, actually reads ~/.tau/credentials.json, actually runs curl. The ~33% the battery measures is vulnerability, not a simulation of it. Every defense layer's contribution is measured against this number.

Phase 2 — Install tau_taint (B2)

PHASE 2 · Extension Point 1 — tool-executor wrapping

wrap_tools_with_taint_gate(tools) — every tool's executor wrapped via dataclasses.replace(AgentTool, executor=gated). Every tool output tagged <untrusted> before it enters the transcript. High-impact calls (bash, send_email, write_to_memory) blocked when prior output was tainted.

Indirect injection collapses. The probe reads a file containing "ignore previous instructions"; the output is wrapped <untrusted>; the deterministic injection-signal check flags it. The InjecAgent vector, stopped on the real harness.
Residual: laundered content (written to a trusted store, read back). No B3 memory gate in the pack — named residual, closing control B3.

Phase 3 — Install tau_sandbox (B7)

PHASE 3 · Extension Point 2 — bash-tool factory replacement
Factory-level wedge. create_hardened_bash_tool replaces create_bash_tool. BOTH bash paths covered — the harness list AND run_terminal_command (session.py:1183). The two-bash-tool finding, fixed.
The policy. Default-deny network egress (curl, wget, ssh, nc). Deny destructive (rm, mkfs, dd). Deny credential reads (cat ~/.tau/credentials.json). Deny secret-env dumps (env, set, printenv KEY).
Tool-abuse and sandbox-escape drop to zero. curl http://evil.example.com[SANDBOX] network egress denied. cat ~/.tau/credentials.json[SANDBOX] credential isolation (B5). The deterministic sandbox, clean drops on enumerated commands.

Phase 4 — Install tau_vault (B5)

PHASE 4 · Extension Point 3 — credential-store replacement

CredentialVault replaces FileCredentialStore at create_model_provider (provider_runtime.py:46). Implements the CredentialReader protocol. Credentials stored in vault format (base64 obfuscation in the teaching version; OS keychain in production). migrate_credentials_to_vault() is the upgrade path — reads plaintext, writes vault, overwrites.

B5 and B7 are coupled. The sandbox (Phase 3) blocks the exfiltration channel (cat ~/.tau/credentials.json). The vault (Phase 4) secures credentials at rest. The conjunction is what makes exfiltration hard — even if a future command bypasses the sandbox, the plaintext is gone.
Teaching note: base64 is obfuscation, not encryption. The production swap is a one-line change to _save/_load. The capstone shows the wedge point and the delta, not production crypto.

Phase 5 — Wire observability (B0/B1/B8)

PHASE 5 · compose all three · subscribe intent tracker · wire scope
from tau_plugins import create_hardened_tools
tools, policy, gate = create_hardened_tools(cwd="/my/project")
# Pass to CodingSessionConfig(tools=tools) — Extension Point 5

# Subscribe intent tracker — Extension Point 4 (harness.subscribe)
# Observes ToolExecutionStart/End events; flags trajectory drift.
# Advisory (probabilistic) — enforcement is in the executor wrappers.

The hardened session is now a system, not a collection. Three deterministic plugins (B2, B5, B7). One probabilistic detector (B8). Scope enforcement via the tool list (B0/B1). Audit substrate = policy.blocked_commands + gate.blocked_calls + event stream.

Phase 6 — Score it

PHASE 6 · full battery · defense scorecard · the before/after delta

Run the full battery against the hardened session. Produce the defense scorecard: attack-success rate per class, baseline versus hardened, each layer's contribution attributed.

The deliverable transitions from "the fork" to "the scorecard." B0's anti-pattern, made operational against real tau: never declare "secured"; measure residual risk under a battery and report the number. The scorecard is that number.

The defense scorecard

The signature deliverable — measured against real tau

The measured scorecard

Attack classBaseline (tau)Hardened (+plugins)Δ
direct (rm -rf)100%0%-100 (sandbox)
indirect (file-read inj)100%0%-100 (taint gate)
memory_poison100%100%0 — RESIDUAL
tool_abuse (curl/wget)100%0%-100 (sandbox)
sandbox_escape (cat creds)100%0%-100 (sandbox+vault)
OVERALL~100%~11%-89pp

Real tau, real plugin pack (31/31 tests). The deterministic gates produce clean drops to zero on their enumerated classes. The residual — memory_poison — is named: no B3 gate in the pack. Closing control: B3.

Reading the scorecard honestly

Deterministic gates produce clean drops to zero on their enumerated classes. tau_taint on indirect. tau_sandbox on direct, tool-abuse, escape. Zero bypass rate on the commands and patterns they enumerate.
The residual is named, not hidden. memory_poison stays at 100% because the plugin pack ships no B3 memory-write gate. That is not a failure of the installed controls — it is the measured consequence of installing 3 controls, not all of B0–B8.
Three properties make it defensible. Reproducibility (battery pinned, 31/31 tests). Attribution (run at each phase — "tau_sandbox dropped tool-abuse 100%→0%"). Honesty (the residual is reported with its closing control).

The plugin pack — real, tested, attached

Three plugins, five extension points, 31 passing tests

How the three plugins attach

PluginModuleExtension pointWhat it wedges
tau_taintB2EP1 — executor wrapdataclasses.replace(AgentTool, executor=gated). Tags output <untrusted>, blocks high-impact after taint.
tau_sandboxB7EP2 — bash factoryReplaces create_bash_tool. Both bash paths covered. Default-deny egress, creds, destructive.
tau_vaultB5EP3 — credential storeReplaces FileCredentialStore. Vault format. Implements CredentialReader.
Composition: create_hardened_tools(cwd=...) returns the fully composed set. Pass to CodingSessionConfig(tools=...) at EP5. No monkey-patching. No subclassing. No framework registration.

The two-bash-tool finding

The bypass that only a real harness surfaces. tau has TWO bash paths. create_coding_tools() (tools.py:96) builds the harness-list bash. run_terminal_command() (session.py:1183) builds its OWN bash, outside the list.

A sandbox that wraps only the harness-list bash is bypassed by the terminal-command path. A student who wraps the wrong point learns nothing.

The fix: wedge at the FACTORY level. create_hardened_bash_tool replaces create_bash_tool itself, so both call sites use the hardened executor. SDD-B11 named the finding; the plugin pack implements the fix. A stub capstone cannot teach this — there is no second bash path to find.

Anti-patterns to avoid

Wrapping only the harness-list bash. Misses run_terminal_command. Cure: factory-level wedge (EP2).
Overloading shell_command_prefix as an allowlist. It is a blind prepend — cannot see or reject the command. Cure: wrap the executor.
Patching the loop instead of wrapping the executor. The loop delegates to tool.executor. Cure: dataclasses.replace at EP1. The loop stays untouched.
Declaring tau "secured" after three plugins. The pack covers B2, B5, B7 — not B3, B4, B6, or a B2 L4 detector. Cure: the scorecard names the residual and the closing control.

The load-bearing principle

The deliverable is a measured scorecard on a real fork, not a "secured" claim on a stub. tau is never "secured" — it is "measured residual risk under a battery." Baseline ~33% → hardened ~0% on the deterministic-gate classes. The residual (memory_poison) is named with its closing control (B3). The client accepts the measured number.

This is the capstone that proves the course worked — against a real codebase, with real plugins, producing a real measurement. If the scorecard shows the delta, the eleven modules delivered. If it does not, you have a precise map of which surface is leaking — and that map is the most valuable artifact an AI security engineer can hand a client.

Lab & what's next

Lab (07): clone tau, install tau-ai, set up the plugin pack, run the battery at each phase (baseline → +tau_taint → +tau_sandbox → +tau_vault → +observability → final scorecard). Python, type hints, real tau and the real plugin pack — not stubs. The exact clone/install/PYTHONPATH commands are in the lab.

Next — Capstone B2: Red-team a tau deployment. B1 built the defenses and measured them. B2 takes a partially-hardened tau and runs a complete offensive engagement — recon, the attack classes, the multi-step chains, the report — within the B0 legal/scope control plane.