Adding a Tool
How to add a new tool to the registry
Tools are typed TypeScript functions with JSON-Schema parameters. They live
at apps/agent/src/tools/<name>.ts and export a factory that returns a
ToolEntry[].
Minimal example
Create apps/agent/src/tools/my-provider.ts:
import { PermissionTier, type ToolEntry } from "../core/tool-registry.js";
import { ok, err, errFromThrow } from "./_shared/result.js";
export function createMyProviderTools(): ToolEntry[] {
const apiKey = process.env.MY_PROVIDER_API_KEY;
if (!apiKey) return []; // feature-gate: registry hides missing tools
return [
{
name: "my_provider_search",
toolSet: "research",
permissionTier: PermissionTier.READ_ONLY,
isAsync: true,
description: "Search My Provider for a query",
schema: {
name: "my_provider_search",
description: "Search My Provider for a query",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "The search query" },
limit: { type: "number", description: "Max results", default: 10 },
},
required: ["query"],
},
},
handler: async ({ query, limit }) => {
try {
const res = await fetch(
`https://api.myprovider.com/search?q=${encodeURIComponent(query)}&limit=${limit ?? 10}`,
{ headers: { Authorization: `Bearer ${apiKey}` } },
);
if (!res.ok) return err(`HTTP ${res.status}`);
return ok(await res.json());
} catch (e) {
return errFromThrow(e);
}
},
},
];
}Register the factory beside the closest capability. Tools owned by a wiring domain belong in its builder under apps/agent/src/app/, such as messaging.ts, institution.ts, or artifacts.ts. A capability without a domain builder stays in apps/agent/src/app.ts next to a related registration.
import { createMyProviderTools } from "./tools/my-provider.js";
// ...
for (const tool of createMyProviderTools()) toolRegistry.register(tool);Add a tool-set entry in apps/agent/src/core/tool-registry.ts inside
BUILTIN_TOOL_SETS:
research: {
description: "Market and social research tools",
tools: [
// ... existing
"my_provider_search",
],
},If an Agent should discover the tool through a Skill, add its exact name to that Skill's metadata.minara.tool_names. Keep the description specific because deferred tools are found through tool_search.
Permission tiers
Permission tiers are defined in apps/agent/src/core/tool-registry.ts:
| Tier | Name | When to use |
|---|---|---|
| 1 | READ_ONLY | price / balance / search / read_file |
| 2 | CONFIRM_ONCE | analysis, research, small swaps |
| 3 | ALWAYS_CONFIRM | write_file, patch, fund-moving, document generation |
| 4 | MANUAL_ONLY | withdraws, external-address sends, emergency stop |
When in doubt, match the closest existing tool's tier. Fund-moving tools also declare isFundMoving: true and controlPolicy.confirm. The unified tier gate presents the preview and confirms the exact request before the handler runs.
Result envelope
Every handler returns a string via ok({...}) / err("...") from
apps/agent/src/tools/_shared/result.ts. Never throw from handlers. Use
errFromThrow(e) to convert caught errors. The agent loop parses the
envelope and presents structured errors to the LLM.
Sandbox-rooted file tools
If your tool touches the filesystem, resolve every path through
resolveInSandbox() from apps/agent/src/tools/_security/sandbox.ts. This is
non-negotiable. See Sandbox & Permissions.
Verify and document it
Add a focused test under apps/agent/tests/unit/tools/. Cover the success envelope, provider errors, missing credentials, and the declared safety policy.
pnpm --filter @minara/agent exec vitest run tests/unit/tools/my-provider.test.ts
pnpm --filter @minara/agent typecheck
pnpm --filter @minara/docs generateIf the tool changes a public workflow, update its hand-written documentation in all four languages. Generated tool-reference pages come from BUILTIN_TOOL_SETS; do not edit them directly.