Every tool in the Harness is a document in the Space under ~/tools/, encoded as canonical DAG-CBOR and content-addressed: the CID of the document is the version of the tool.
type ToolDocument = {
name: string
kind: 'builtin' | 'lambda'
summary: string // the one line that appears in the Space index
description: string // the model-facing description
input: JSONSchema
output?: JSONSchema
source?: string // lambdas only
runtime?: 'typescript' | 'python'
binding?: string // builtins only — the runtime executor id
}Builtins carry a runtime binding — search, web_search, navigate, execute. Their documents materialize lazily: the first listing, index build, or read creates or refreshes them, and refresh is a no-op when the CID hasn't drifted.
Lambdas are agent-authored. write ~/tools/<name> saves source plus schemas; authoring, testing, and saving all flow through the same verbs as everything else. Builtins cannot be overwritten or deleted by a lambda write — they are disabled through grants instead.
Names must match ^[a-z][a-z0-9_-]{1,63}$; source is capped at 256 KiB and the description at 16 KiB.
The contract — description plus input/output schemas — is what read ~/tools/<name> returns, and it is the same text the desktop Tools tab shows an owner, alongside the source and the content address.
The Space index
Every run's system prompt carries a compact <space> block, budgeted at 2 KiB: one line per tool, memory's top level, active triggers. The agent always knows what it could use without carrying full schemas for any of it. This is the mechanism that holds the prompt surface constant as the tool set grows — the whole point of the verb collapse.
Touch-expand and promotion
A tool starts collapsed to its summary line. Two things expand it:
read ~/tools/<name> returns the full contract.
A call to an unexpanded tool — or a call with invalid input — returns the contract as the result instead of an error, with validation errors attached. The model reads it and retries. The code's comment: "Touch-expand: a miss returns the contract instead of failing; the retry executes."
Once a contract has entered the transcript, the tool is promoted: it becomes a first-class provider tool for the rest of the thread. Promotion is derived purely from the Log's durable tool_call events — the transcript is the pin — so restarts, park-resume, and replay reconstruct the active set identically. Events with actor: 'user' are deliberately skipped: a user's palette call must not silently reshape the agent's tool set.
The security filter matters more than the mechanism. Because a model can durably record any tool name it hallucinates, the promoted set is intersected with the agent's enabled callables before anything reaches the provider. The source says why:
> SECURITY: promotion must never exceed the enabled callable set — a hallucinated call {tool: 'bash'} durably stores that name, and an unfiltered allowlist would hand it to Pi, activating Pi's own host bash/edit builtins outside the sandbox.
Grants
Grants are deliberately few: the callable set (search, web_search, navigate, execute) and publish (signed public writing). The verbs themselves are never grants — they are always on — and memory writes are never gated. An agent with no tools array at all gets every callable and the publish grant; narrowing is opt-in, which is worth knowing when you create one.
A lambda call requires the same execute grant its runtime needs, so authoring a tool is not a way around an owner who turned code execution off. See Agents and Grants.
Related
The Five Verbs — call and touch-expand in context
Execution — where lambda source runs
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime