@kiliaosi/toll
v0.1.0
Published
Framework-agnostic Tool Runtime — governance (discovery, execution, restriction, optimization, audit) for AI agent tools.
Maintainers
Readme
@kiliaosi/toll
Framework-agnostic Tool Runtime — a governance layer for AI agent tools.
toll = tool + toll (a checkpoint). Every tool call passes through the checkpoint.
It is not another agent framework and does not take over the model↔tool loop. It sits one layer below that loop and turns a raw tool call into a governed one — discovery, restriction, optimization, and audit — then hands the governed tools to whatever loop you already use (Claude Agent SDK, Vercel AI SDK, a raw MCP client, or your own loop).
┌─────────────────────────────────────┐
│ Agent Loop (Claude SDK, LangGraph…) │ someone else's job: the model↔tool loop
├─────────────────────────────────────┤
│ Tool Runtime ← toll lives here │ discovery / restriction / optimization / audit
├─────────────────────────────────────┤
│ Raw Tools (fs, child_process, rg) │ thin wrappers
└─────────────────────────────────────┘Why
Every SDK bakes tool governance into its own loop and couples it there — switch loops and you rewrite the governance. toll makes one policy (a danger model + allow/deny + human confirmation + an audit trail) that you carry across frameworks, and that can govern a host's own native tools via a hook.
Install
npm install @kiliaosi/tollTrack 1 — Govern your Claude Code
This is the capability nothing else gives you: Claude Code's own Bash/Write/Edit never flow through an MCP server, so an MCP server cannot restrict them. A PreToolUse hook can. toll ships one as a CLI.
Add to .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "npx -y @kiliaosi/toll hook" }
]
}
]
}
}Now every native tool call is judged by toll's danger policy before it runs. Bash is graded by its actual command (not just the name): git status passes, rm -rf build is blocked. Every decision is written to an append-only JSONL audit log.
Configure via env (no code):
| Env | Values | Default | Meaning |
|-----|--------|---------|---------|
| TOLL_MAX_DANGER | safe | mutating | destructive | mutating | Danger ceiling. Calls above it are blocked. |
| TOLL_ASK_ON_DENY | 1 | off | Turn a hard block into an interactive prompt instead. |
| TOLL_AUDIT_LOG | path | ./toll-hook-audit.log | Where the audit trail goes. |
⚠️ Bash command-grading is a heuristic to add friction on the obviously-dangerous case, not a sandbox. A determined command can evade pattern matching. Use it as defense-in-depth.
Need a custom policy? Import the pieces and build your own hook entry:
import {
createHookRuntime, decidePreToolUse, parsePreToolUseEvent, readStdin,
bashCommandDanger, audit, restriction, fileSink,
} from '@kiliaosi/toll';
const rt = createHookRuntime({
middleware: [
audit({ sink: fileSink('./audit.log') }),
restriction({ maxDanger: 'mutating', deny: ['WebFetch'] }),
],
});
const event = parsePreToolUseEvent(await readStdin());
const decision = await decidePreToolUse(rt, event, { classify: bashCommandDanger });
process.stdout.write(JSON.stringify(decision));Track 2 — Provide governed tools to your agent
Build a runtime from tools + middleware, then adapt it to your framework. The same GovernedTool works everywhere.
import {
createToolRuntime, builtinTools, restriction, audit, fileSink,
} from '@kiliaosi/toll';
const rt = createToolRuntime({
tools: builtinTools(), // read, glob, grep (safe) + write, bash (destructive)
middleware: [
audit({ sink: fileSink('./audit.log') }),
restriction({
maxDanger: 'mutating',
confirm: async (call) => askTheUser(call), // gate destructive calls
}),
],
});
await rt.invoke('read', { path: 'package.json' }); // runs the full governance chainAdapt to a framework — the runtime is unchanged, only the adapter differs:
// Claude Agent SDK
import { toClaudeSdkServer } from '@kiliaosi/toll';
const server = toClaudeSdkServer(rt);
// query({ prompt, options: { mcpServers: { toll: server } } })
// Vercel AI SDK
import { toVercelTools } from '@kiliaosi/toll';
const tools = toVercelTools(rt);
// generateText({ model, prompt, tools })
// A generic MCP server (any MCP client can connect)
import { serveMcp } from '@kiliaosi/toll';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
await serveMcp(rt, new StdioServerTransport());Track 3 — Adopt & govern a third-party MCP server
Wrap tools you don't control with your own checkpoint. Adopted tools are fail-closed by default: without a confirm, only read-only tools run.
import { fromMcpServer, restrictAdopted, createToolRuntime, audit, fileSink } from '@kiliaosi/toll';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const { client, tools } = await fromMcpServer(
new StdioClientTransport({ command: 'node', args: [someServerEntry] }),
{ prefix: 'fs_' },
);
const rt = createToolRuntime({
tools,
middleware: [audit({ sink: fileSink('./audit.log') }), restrictAdopted()],
});The governance pipeline
An invoke flows through composable middleware. Each is optional and independent.
| Phase | Role |
|-------|------|
| discovery | Which tools are visible in this context. |
| restriction | allow/deny lists, a safe < mutating < destructive danger ceiling, human confirmation. |
| execute | Timeout / retry around the tool's own handler. |
| optimization | Output shrinking — transparent by default (byte-identical); opt in via a compress hook that delegates to a dedicated compressor. toll ships no compression algorithm of its own. |
| audit | Full lifecycle trace of every call — args, each phase's decision, timing, result — to an append-only sink. Must be the outermost middleware. |
type Middleware = (call, next) => Promise<ToolResult>;
// composed outer→inner: [audit, restriction, execute, optimization]ToolResult is MCP-shaped ({ content, isError? }), so it crosses the MCP / Claude SDK boundary with zero conversion.
Governance boundary (host vs toll)
When both toll and a host (Claude Code) can gate a call, they are domain-separated, never stacked:
- Host-native tools (Claude Code's own Bash/Write/…) → governed by toll via the PreToolUse hook.
- Self-registered / adopted tools → governed by the toll runtime directly.
The two paths are mutually exclusive: a call is judged by toll once. This avoids the "host allowed it but toll blocked it" double-judgment. See ARCHITECTURE.md §7.5.
License
MIT © kiliaosi
