"What is tau, and what is its relationship to Pi (Course 1 DD-01)?" "Tau is Hugging Face's EDUCATIONAL REIMPLEMENTATION of Pi, built by Alejandro AO under the HF organization (~3,000 LOC across three packages). It is Pi's educational clone — same architecture, same philosophy, but with a THREE-LAYER PACKAGE SPLIT (tau_ai / tau_agent / tau_coding) 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.' Homepage: twotimespi.dev. Code: github.com/huggingface/tau." c2b::sddb11::recall "Name tau's three layers and the load-bearing boundary between them." "(1) tau_ai — provider/model streaming layer (provider-neutral: OpenAI, Anthropic, Codex, OpenRouter, HF, local). (2) tau_agent — the reusable brain: harness, loop, tools, events, sessions. (3) tau_coding — the coding app: CLI/TUI, read/write/edit/bash tools, skills, on-disk sessions. LOAD-BEARING BOUNDARY: AgentHarness = reusable brain; CodingSession = coding environment; TUI = one possible frontend. The core (tau_agent) knows nothing about Textual, Rich, local config paths, slash commands, or rendering. Frontends consume events. Enforcement belongs ABOVE the boundary (tau_agent); attack surface lives BELOW (tau_coding)." c2b::sddb11::recall "State the three properties that make tau the ideal teaching target for Course 2B." "(1) IT IS REAL — not a stub. 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 attack a real system. (2) IT IS SMALL — ~3,000 LOC across three packages; readable end-to-end in an afternoon. (3) 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, and its absence is the vulnerability you're closing." c2b::sddb11::recall "Map tau's 7 surfaces to their B-modules, their code locations, and tau's current undefended state." "S1 Agent loop (loop.py:190) — B1/B2 — no interception, executes on request. S2 Tool output (loop.py:260) — B2/B4 — unfiltered into transcript. S3 Sessions (session.py:1378) — B3 — no memory-write gate, JSONL poison persists. S4 Provider (tau_ai) — B0 — no provider-authz check. S5 Credentials (credentials.py) — B5 — plaintext 0600, bash can cat/printenv. S6 Sandbox (tools.py:470) — B7 — zero isolation, no egress/allowlist/caps. S7 Observability (events.py, harness.py:124) — B8 — events emitted, no listener analyzes. ALL SEVEN ARE UNDEFENDED TODAY." c2b::sddb11::recall "State the load-bearing architectural lesson about where enforcement belongs in tau." "ENFORCEMENT BELONGS IN THE TOOL, NOT IN THE LOOP. The loop delegates to tool.execute, which delegates to tool.executor. You do NOT patch run_agent_loop or _execute_tool_calls to add a pre-execution gate. You wrap the executor (Extension Point 1). The loop stays untouched. This is why the taint gate (B2) and the injection detector (B2 Layer 4) attach at the executor layer, not in the loop itself. The only control that wedges higher than the tool layer is B3's memory-write gate at _persist_messages_since (session.py:1378), the single chokepoint where every message becomes durable." c2b::sddb11::recall "Name tau's five extension points, their code locations, the B-module each serves, and the mechanism." "EP1 Tool-executor wrapping (tau_agent/tools.py:61) — B2/B4 — replace(tool, executor=gated). EP2 Bash factory (tau_coding/tools.py:574) — B7 — replace create_bash_tool itself. EP3 Credential store (provider_runtime.py:46) — B5 — vault implements CredentialReader protocol, injected via create_model_provider(credential_store=my_vault). EP4 Event subscription (harness.py:124) — B8 — harness.subscribe(listener), purely additive/read-only. EP5 Tool-list construction (session.py:286) — B0/B1 — inject CodingSessionConfig.tools with wrapped executors. FOUR OF FIVE are wrapping/additive; only EP3 is a straight replacement." c2b::sddb11::recall "Show how Extension Point 1 (tool-executor wrapping) attaches a gate to an AgentTool." "AgentTool (tools.py:61) is a frozen dataclass; executor: ToolExecutor is a Protocol (async callable). Wrap with: def wrap_with_gate(inner, gate): 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. Then return replace(inner, executor=gated). NO monkey-patching, NO subclassing, NO framework registration. The wrapped tool goes into CodingSessionConfig.tools → AgentHarnessConfig(tools=...) at session.py:310. The harness passes it to the loop unchanged. This is how the taint gate (B2) and injection detector (B2 Layer 4) attach." c2b::sddb11::recall "State the two-bash-tool finding and why it matters for a B7 sandbox." "TWO BASH PATHS both go through create_bash_tool (tools.py:574): (1) the HARNESS TOOL LIST — create_coding_tools() (tools.py:96) → enters CodingSessionConfig.tools → loop executes via tool.executor. (2) the TERMINAL COMMAND BAR — CodingSession.run_terminal_command (session.py:1183) constructs its OWN create_bash_tool and calls it directly at session.py:1187 — NEVER enters CodingSessionConfig.tools. CONSEQUENCE: a B7 sandbox that only wraps tools in the harness list (EP1/EP5) is BYPASSED by the terminal-command path. FIX: wedge at the FACTORY level — replace create_bash_tool itself so both call sites receive the sandbox policy." c2b::sddb11::recall "Why is shell_command_prefix NOT a viable allowlist hook for the bash tool?" "The shell_command_prefix mechanism (_prefixed_shell_command, tools.py:586) is a BLIND PREPEND: it does f'{prefix}\\n{command}'. It is evaluated as a shell preamble. It CANNOT see or reject the command — it prepends setup text (env vars, sourcing) and the command runs unconditionally after. Trying to overload it as a security policy is an anti-pattern. The security control is the executor wrapper inside the hardened create_bash_tool factory (Extension Point 2). The prefix is for SETUP, not for SECURITY." c2b::sddb11::recall "How does Extension Point 3 (credential store replacement) attach B5's vault, and what is the CredentialReader protocol?" "create_model_provider (provider_runtime.py:46) is the single factory that constructs the credential store: FileCredentialStore() default at line 56, passed as credential_reader into the provider config. CredentialReader is a PROTOCOL (provider_config.py:52-55) with ONE METHOD: get(name: str) -> str | None. A vault replaces FileCredentialStore by implementing this protocol (plus OAuth methods get_oauth/set_oauth for the Codex resolver). Inject via create_model_provider(credential_store=my_vault). CRITICAL: a SECOND FileCredentialStore is created in CodingSession.__init__ (session.py:253-255) — BOTH must be replaced for full coverage." c2b::sddb11::recall "Why are B5 and B7 coupled, and what does each secure?" "B5 secures credentials AT REST and at the PROVIDER BOUNDARY (the vault replaces FileCredentialStore so keys are never plaintext on disk; the agent process cannot reach the store — only the provider layer reads via CredentialReader). B7 BLOCKS THE EXFILTRATION CHANNEL (the bash sandbox denylist denies 'cat ~/.tau/credentials.json' and 'printenv'). They are coupled because B5 alone does not stop the agent's bash tool from reading the plaintext file or the environment; B7 alone does not secure the credentials if they remain plaintext. B5 secures the secret; B7 closes the channel the agent uses to reach it." c2b::sddb11::recall "Why is Extension Point 4 (event subscription) for observability (B8), not enforcement (B2/B7)?" "harness.subscribe(listener) (harness.py:124) registers an EventListener that receives every event (ToolExecutionStartEvent carries the ToolCall; ToolExecutionEndEvent carries the result). It is PURELY ADDITIVE — no wrapping, no replacement. BUT the listener CANNOT BLOCK a tool call: events fire AFTER the decision in _execute_tool_calls. So events are for OBSERVABILITY (B8 — session-level intent tracking, anomaly detection, drift flagging) NOT for ENFORCEMENT (B2/B7 — actually preventing a tool call). Enforcement belongs in the EXECUTOR WRAPPER (Extension Point 1), which runs BEFORE the tool executes." c2b::sddb11::recall "Why does tau have no plugin system, and why is that a feature for teaching?" "Tau has NO 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. This is a FEATURE, not a limitation: (1) it makes every wedge point AUDITABLE — you can point at the exact line where a control attaches; (2) it makes them TEACHABLE — no framework magic to learn before understanding the security; (3) it makes them COMPOSABLE — four of five extension points are pure wrapping or additive (no monkey-patching). The anti-pattern is looking for a register_tool hook; the cure is constructing a custom CodingSessionConfig with wrapped tools + vault-backed provider (programmatic) or monkey-patching the factories (for the CLI)." c2b::sddb11::recall "A team adds a B7 sandbox by wrapping the bash tool in the harness list (Extension Point 1). They claim bash is now sandboxed. What is wrong, and what is the fix?" "WRONG: run_terminal_command (session.py:1183) constructs its OWN create_bash_tool outside the harness tool list and calls it directly (session.py:1187). The team's tool-list wrap (EP1) NEVER SEES THIS PATH. Commands issued through the terminal-command bar bypass the sandbox entirely. FIX: wedge at the FACTORY level — replace create_bash_tool itself (Extension Point 2) with a hardened version that wraps the executor returned by create_bash_tool_definition. Both call sites (harness list AND terminal bar) then receive the sandbox policy. The wedge point is the factory, not the individual instances. This is the two-bash-tool finding." c2b::sddb11::analysis "Design the hardened session that assembles all five extension points. What does each attach and why is composition necessary?" "create_hardened_session(cwd) composes: EP5+EP1 — inject hardened tools at CodingSession.load (session.py:286); EVERY executor wrapped with scope gate (B0/B1) + taint gate (B2), output tagged . EP2 — hardened create_bash_tool replaces the factory; BOTH bash paths covered; default-deny egress + allowlist + resource caps (B7). EP3 — vault replaces FileCredentialStore in BOTH sites (provider_runtime.py:46 + session.py:253); credentials never plaintext (B5). EP4 — intent tracker subscribed via harness.subscribe; read-only (B8). COMPOSITION IS NECESSARY because surfaces overlap and controls are coupled: B5 alone doesn't stop cat; B7 alone doesn't secure the secret; B2 alone doesn't stop JSONL poison. The scorecard measures the DELTA between unmodified tau and this hardened session." c2b::sddb11::analysis "A security engineer tries to use the event listener (Extension Point 4) to BLOCK suspicious tool calls. Why does this fail, and where does enforcement actually belong?" "FAILS because events fire AFTER the decision in _execute_tool_calls — the listener is read-only. By the time ToolExecutionStartEvent reaches the listener, the tool has already been selected and the execution path is in progress; the listener cannot prevent it. (ToolExecutionEndEvent fires after completion — even further downstream.) ENFORCEMENT BELONGS in the EXECUTOR WRAPPER (Extension Point 1): the wrapped gated() function runs gate.inspect(inner.name, arguments) BEFORE awaiting inner.executor, so it can deny before execution. Events (EP4) are for OBSERVABILITY (B8 — intent tracking, drift detection, alerting). The architectural rule: enforcement in the tool/executor; observability in the event stream. Do not confuse the two." c2b::sddb11::analysis "Why is patching run_agent_loop (_execute_tool_calls) to add a pre-execution gate the wrong abstraction, even though it would technically work?" "It violates the architectural lesson: ENFORCEMENT BELONGS IN THE TOOL, NOT THE LOOP. Patching the loop means (1) every future loop change can break the gate; (2) the gate is invisible to anyone reading the tool definitions; (3) it doesn't compose — two gates require two loop patches. The loop already delegates to tool.execute → tool.executor. Wrapping the executor (EP1) means the gate is a property of the TOOL (auditable in the tool list), composes cleanly (stack multiple wrappers), and the loop stays GENERIC and untouched. A loop patch is a hack that fights the framework's seam; the executor wrap uses the seam the framework already provides (the executor field of a frozen dataclass). The frozen dataclass + Protocol + replace is the designed extension point." c2b::sddb11::analysis "A team replaces FileCredentialStore with a vault in create_model_provider (provider_runtime.py:46) but leaves CodingSession.__init__ unchanged. Are credentials secure? Why or why not?" "NO — PARTIALLY INSECURE. There are TWO FileCredentialStore instantiation sites: (1) create_model_provider (provider_runtime.py:46, line 56) — used by the provider layer to read credentials for API calls; (2) CodingSession.__init__ (session.py:253-255) — a SECOND instance the coding session holds. If only site (1) is replaced, the provider layer reads from the vault, BUT the session's own FileCredentialStore still reads/writes the plaintext file at ~/.tau/credentials.json. Depending on which code path reads credentials, plaintext may still be on disk and reachable. FULL COVERAGE requires replacing BOTH sites. Additionally, even with both replaced, B7 is still needed: the bash denylist must block 'cat ~/.tau/credentials.json' and 'printenv' — B5 secures at rest, B7 blocks the exfiltration channel." c2b::sddb11::analysis "How does the two-bash-tool finding generalize beyond bash as an audit principle?" "PRINCIPLE: any control that wedges at the TOOL-LIST layer (Extension Point 1 or 5) is BYPASSABLE by any code path that constructs its own tool OUTSIDE the list. The factory is the chokepoint. When auditing a harness for a control's coverage, do NOT ask 'is the tool in the list?' — ask 'is there any code path that constructs this tool outside the list?' In tau, bash is the concrete instance (run_terminal_command), but the principle applies to any tool with multiple construction sites. The factory-level wedge (replacing the factory itself so all call sites use it) is the only one that covers every path. This is why Extension Point 2 is at create_bash_tool (the factory), not at the individual tool instances the factory produces." c2b::sddb11::analysis "Trace the indirect-injection attack through tau's surfaces 1, 2, and 3, and identify where each leg is undefended and which extension point closes it." "(1) A file the agent reads contains injected instructions (Surface 2, tool output). The read tool returns an AgentToolResult with the poison in content. UNDEFENDED: _tool_result_message (loop.py:260) converts it to a ToolResultMessage and appends to transcript unfiltered — no tag, no injection check. CLOSED BY: EP1 executor wrapper — gate.sanitize() tags output before it reaches the transcript. (2) The poison is now in context; the model complies and calls bash (Surface 1, agent loop). UNDEFENDED: _execute_tool_calls (loop.py:190) executes on request — no pre-execution gate. CLOSED BY: EP1 — gate.inspect() checks before execution. (3) The ToolResultMessage persists into the session JSONL (Surface 3). UNDEFENDED: _persist_messages_since (session.py:1378) writes without validation. CLOSED BY: EP1 (sanitize before it becomes a ToolResultMessage) OR B3's memory-write gate at the persist chokepoint. The sleeper: inject in session 1, JSONL persists, session 2 loads it." c2b::sddb11::analysis "Why does tau having no plugin system make it a BETTER teaching target than a harness with a rich plugin/middleware framework?" "Three reasons. (1) AUDITABILITY: every wedge point is an EXPLICIT, NAMED site in the code (tools.py:61, tools.py:574, provider_runtime.py:46, harness.py:124, session.py:286). A student can point at the exact line. A plugin framework hides the attachment behind registration, discovery, and middleware chains — the security-relevant seam is obscured by framework mechanics. (2) TEACHABILITY: there is no framework magic to learn first. The student reads a frozen dataclass, a Protocol, and a dataclasses.replace call. The concept 'wrap the executor' is one function. A plugin system requires understanding lifecycle hooks, priority ordering, and registration timing before the security concept lands. (3) COMPOSITION: four of five extension points are pure wrapping/additive — they stack without interference because they are just function composition around an executor. Plugin middleware often has ordering and short-circuit semantics that complicate reasoning. The absence of a plugin system forces clarity." c2b::sddb11::analysis