Tool Design & the Tool Contract

Module 2 · Master Course — Harness Engineering

90 minutes · 4 sub-sections: Architecture · Writing Tools · Dispatch · Security

Where most security vulnerabilities live.

The tool surface

Every tool is a way for the agent to affect the world — and a way for the world to affect the agent. The tool surface is bidirectionally an attack surface.

Module 1 gave you the loop's executeTool extension point. This module goes inside it. By the end: the five-part contract, the dispatch sequence, the three injection vectors, and the defenses Module 11 harvests.

2.1 Tool Architecture Patterns

The five-part tool contract

PartPurposeWhat goes wrong if missing
Namewhat the model invokesambiguous → wrong-tool calls
Descriptiona PROMPT — model reads to decide whenvague → never called, or called wrong
Input schematypes, bounds, defaultsparse failures, untyped garbage, injection
Implementationwhat the harness executesbugs, side effects, security holes
Error returnwhat goes back on failurethrown exceptions → model hallucinates success

Every tool in every production harness has all five. Missing any degrades reliability or security.

Tool descriptions are prompts

Bad: "Reads a file."
The model doesn't know formats, size limits, or result shape.
Good: "Reads a UTF-8 text file from the workspace. Returns full contents under 10k lines; else first 500 + line count. Use for inspecting file contents. NOT for binary — use read_binary. Paths relative to workspace root."
The model reads tool descriptions to decide when and how to call. A description is a prompt, not documentation. Marketing language wastes the model's attention.

The tool-count decision

The Vercel finding: cutting 80% of their agent's tools produced better results. Fewer tools = less decision noise = better selection.
Pi (4 tools)Claude Code (40+)
Decision noiseminimalhigh (mitigated by dynamic registration)
Capabilitypersonal assistantenterprise multi-channel
Correct forthin use casesthick use cases

The question isn't "how many tools?" It's: what's the minimum tool set that maximizes capability without decision noise? Most teams err on too many.

Registration patterns

PatternTradeoff
Static (Pi: 4 fixed)fewer choices, lower injection surface; less extensibility
Dynamic (Claude Code)right tools at right time; harder to audit
MCP (external servers)decoupled, swappable; NEW attack surface (M11, S12)
Schema-first (Pydantic/Zod)eliminates parse failures; boilerplate
Free-text dispatch (old Aider)simple; brittle — model drift breaks parsing

2.2 Writing a Great Tool

Output formatting: the 67.6% rule

Module 0.3's anchor: tool outputs are 67.6% of context. Output formatting is the highest-leverage decision in tool design.

Rule 1: Truncate aggressively. Every large-output tool has a truncation policy. Visible: truncated: true + guidance.
Rule 2: Structure the output. [{path, line, preview}], not raw grep dumps. More token-efficient, more model-legible.
Rule 3: Include action guidance. "truncated: showing 50 of 312. Narrow your pattern." Prompt engineering inside the tool result.

Idempotency

The model retries. Transient errors, timeouts, malformed first calls — the model's response is to call again. Tools the model might retry must be safe to call twice.
ToolIdempotent?
read_file✅ yes (reading twice is harmless)
write_file⚠️ if same content; problematic if model retries with different content
bash❌ almost never (git commit twice = 2 commits)
send_email❌ never (2 emails sent)

For non-idempotent tools: deduplicate via request ID, content-hash check, or "already sent" guard. Idempotency is a tool-design responsibility.

Error returns: never throw, always return

A thrown exception gives the loop nothing. A structured error lets the model self-correct next turn.
// WRONG — model gets generic "tool failed"
throw new Error("search failed");

// RIGHT — model knows what failed and whether to retry
return {
  ok: false,
  error: "search failed: timeout after 5000ms",
  retryable: true   // → model retries with backoff
};

Four categories (Module 7): transient (retry) · LLM-recoverable (self-correct) · user-fixable (interrupt) · fatal (halt). The tool provides the info; the loop categorizes.

2.3 Tool Routing and Dispatch

The 7-step dispatch sequence

Model emits tool_use
  │
  1. Parse (structured JSON or regex)
  │    └ parse failure → precise error to model
  2. Validate name against registry
  │    └ unknown → error
  3. Schema-validate inputs
  │    └ invalid → precise error ("pattern required, string; got number")
  4. Permission check (Module 6)
  │    └ denied → permission error
  5. Execute implementation
  6. Format result (truncate + structure)
  7. Return to model as tool result in context

Each step is a failure point AND an injection point. Structured output (JSON tool_use) eliminates step-1 parse failures.

MCP: external tool servers

Gains: decoupled (any language), swappable (change servers without changing harness), ecosystem (any MCP server ↔ any MCP harness).
Costs: a NEW attack surface. Tool definitions move outside the harness's trust boundary. An MCP server is supply-chain code you didn't write.

MCP = ASI04 (Agentic Supply Chain) vulnerability if trusted blindly. Module 11.2 + Course 2 S12 cover tool-definition poisoning, evil-twin attacks, signed manifests.

2.4 Tool Security

The tool surface is the attack surface

Three injection vectors

VectorMechanismDefense
1. Tool OUTPUTattacker controls file/page/API response; contains instructionsuntrusted-content tagging
2. Tool DEFINITION (MCP)malicious server's description has hidden instructionssigned manifests + runtime verify
3. Dispatch-timecrafted output with fake closing tag corrupts parsingstructured output (JSON), not text parsing

Vector 1 is the most common and most dangerous. Module 11.3 implements the defense; the lab verifies it defeats a crafted payload.

Capability-based permissions

Defense in depth: tools declare required capabilities; harness checks before executing.

const tools = [
  { name: "read_file",  capabilities: ["fs:read"],  ... },
  { name: "write_file", capabilities: ["fs:write"], ... },
  { name: "bash",       capabilities: ["shell"],    ... },
  { name: "search_web", capabilities: ["network"],  ... }
];

function executeTool(tc, agentPerms) {
  const tool = registry[tc.name];
  const allowed = tool.capabilities.every(c => agentPerms.has(c));
  if (!allowed) return { ok: false, error: "permission denied" };
  return tool.execute(tc.input);
}

A subagent with {fs:read} cannot call bash — regardless of model requests. Permission at the dispatch boundary, not the prompt. Module 0.2's governance principle, at the tool layer.

Five anti-patterns

Untyped tool. No schema. Cure: Pydantic/JSON Schema, always.
Marketing description. "Powerful search tool." Cure: precise inputs/outputs/when-to-use.
Throwing tool. Exceptions on error. Cure: structured {ok, error, retryable}.
Unbounded tool. Full output, no truncation. Cure: 67.6% rule — truncate visibly.
Trusted MCP server. Consumed without verification. Cure: untrusted-code treatment (M11.2).

Takeaways

  • Five-part contract: name, description, schema, implementation, error return.
  • Descriptions are prompts. Precise inputs/outputs/when-to-use; no marketing.
  • Fewer tools often wins. The Vercel finding. Decision noise is real.
  • Never throw; always return. Structured errors let the model self-correct.
  • Three injection vectors. Output (most common), definition (MCP), dispatch. Defenses previewed; Module 11 implements.

Next: Module 3 — Context Management. The hardest engineering problem in production harnesses.