Hooks became tools
A hook was the one way anything outside the chain engine ever got called. The contract was a single Go interface — Exec, Supports, GetSchemasForSupportedHooks, GetToolsForHookByName — and every capability satisfied it. SimpleRepo held in-process hooks in a map: local_fs for reads, writes and searches inside a configured directory, local_shell behind an opt-in flag, webhook for any HTTP endpoint, js_execution for sandboxed JavaScript, plus echo and print. PersistentRepo looked a name up in the database and dialled whatever HTTP service it found registered there. MultiRepo composed any number of repos into one flat namespace by asking each in turn what it supported and routing the call to the first that claimed the name. A chain task reached one with handler: "hook" and a name; the engine neither knew nor cared which implementation answered. The vocabulary was older than the code — the April 2025 MVP’s chain format already carried a hook call as a first-class task type alongside model execution.
The registry landed in August 2025 — a year before there was an agreed word for it, and while tool calling was still something you worked around per model rather than relied on. Remote hooks were a database row, not a plugin: name, endpoint URL, HTTP method, timeout in milliseconds, headers, body properties, and a protocol type validated on write. The protocol type selected one of five ProtocolHandler implementations, each exactly two methods — build the request body, parse the response body. openai sent an OpenAI FunctionCall with its arguments as a JSON string and took the whole reply back. openai-object sent the arguments as a real object instead. ollama sent an object and read message.content out of the response. langserve-openai sent the OpenAI shape and unwrapped LangServe’s output envelope. langserve-direct sent the bare argument map and took the bare reply. Every unwrap that failed said so with the offending body quoted back rather than returning a plausible nil, and each protocol had its own end-to-end API test driving a real chain against a mock server whose request validator asserted the exact bytes on the wire — so a protocol could not quietly change shape. A hook that served an OpenAPI 3 document skipped the hand-written shape entirely: the engine fetched {endpointUrl}/openapi.json, walked every path and method, derived a tool name per operation, and at call time mapped the model’s arguments back into path, query, header, and body parameters. Registering an internal API as an agent capability was a POST and a URL.
By September 2025 the same registered hook was handing the model an OpenAI-shaped tool schema, taking a tool call back, and executing it — the repo interface grew GetToolsForHookByName and the boundary did not move. That turned out to be the whole point. When MCP support landed in March 2026 it landed as one more provider behind the identical interface: an MCP tool’s inputSchema mapped straight onto the engine’s own Tool type, and parameters the runtime injected on the server’s behalf were stripped out of properties and required before the schema ever reached the model, so a credential a server needed never appeared as a field the model could invent. Nothing above the interface changed. Which hooks a task could see was resolved by an allowlist grammar — * for everything, exact names, !name exclusions combined with * — and a second allowlist attached at runtime was intersected with the task’s, stricter side winning, so a chain author could never widen what the caller had already narrowed. Approval sat on the same seam: HITLWrapper decorated any repo at all, evaluating a policy per tool call, passing allows straight through, blocking approvals on a human, and answering denials with a sentence the model could act on — “please ask for clarification or try a different, less destructive approach” — rather than an error that killed the run. The space converged on tools as the word, and on MCP as the wire format — donated to the Linux Foundation at the end of 2025, months after this contract shipped. Nothing had to be rebuilt to meet it. The boundary that had to exist was the same boundary either way, because the problem was the same problem: a model needs a named, schema-described capability it can ask for, an executor that runs it under a policy, and a result that comes back as context. The naming caught up with the shape.
The hook boundary as the docs shipped it: a chain task names its hooks, each hook returns its tool schemas, the schemas go to the model with the prompt, and the model’s tool call comes back through the executor.

What it proved
- One repo interface absorbed an industry-wide renaming without moving. Local hooks, registered HTTP services, OpenAPI-discovered operations, and MCP servers were four implementations of the same four methods — adding MCP changed nothing above the seam.
- Five wire protocols fit behind two methods. Build a request, parse a response: that was the entire per-protocol surface, and every one of the five had an end-to-end test asserting the exact bytes it put on the wire.
- Schema discovery beats schema registration. Fetching
/openapi.jsonand deriving one tool per operation meant a team’s existing internal API became an agent capability with aPOSTand a URL, and stayed correct when the API changed. - Allowlists that compose by intersection are safe to hand to chain authors. A task’s allowlist could only ever narrow the caller’s, never widen it, so a chain pulled from anywhere could not grant itself a capability the runtime had withheld.
- The approval gate belongs at the capability edge, not at the model. Wrapping the repo rather than the executor put every tool call — in-process, remote, OpenAPI, MCP — behind one policy, and made a denial a message the model could reason about instead of a failure that ended the run.