Skip to content

Tools & Permissions

Tools define what an agent can do. Permissions control what it is allowed to do.

Declares a tool’s schema without an executor. Used for validation and documentation.

import { ToolDeclaration } from "@witqq/agent-sdk";
import { z } from "zod";
const searchDeclaration: ToolDeclaration<{ query: string }> = {
name: "search",
description: "Search the knowledge base",
parameters: z.object({
query: z.string().describe("Search query"),
}),
needsApproval: false,
metadata: {
category: "retrieval",
tags: ["search", "read-only"],
},
};

Fields:

FieldTypeRequiredDescription
namestringyesUnique tool identifier
descriptionstringyesShown to the LLM
parametersz.ZodType<T>yesZod schema for arguments
needsApprovalbooleannoTriggers permission request if true
metadata{ category?, icon?, tags? }noArbitrary metadata

Extends ToolDeclaration with an execute function. This is what agents call.

import { ToolDefinition, ToolContext } from "@witqq/agent-sdk";
import { z } from "zod";
const searchTool: ToolDefinition<{ query: string; limit?: number }> = {
name: "search",
description: "Search the knowledge base",
parameters: z.object({
query: z.string(),
limit: z.number().optional().default(10),
}),
execute: async (params, context?: ToolContext) => {
// context.sessionId available if passed by runtime
const results = await db.search(params.query, params.limit);
return results;
},
};

Passed as the second argument to execute.

interface ToolContext {
sessionId: string;
custom?: Record<string, unknown>;
}

Union type accepting either a declaration or a definition:

type ToolDefinitionLike<T> = ToolDeclaration<T> | ToolDefinition<T>;

Use isToolDefinition() to check if a tool has an executor:

import { isToolDefinition } from "@witqq/agent-sdk";
if (isToolDefinition(tool)) {
const result = await tool.execute(args);
}

Tools are passed via AgentConfig.tools (defaults for all runs) or RunOptions.tools (per-call override):

const config: FullAgentConfig = {
systemPrompt: "You are a helpful assistant.",
tools: [searchTool, writeTool],
};
// Override per call
const result = await agent.run("Find X", {
model: "gpt-4.1",
tools: [searchTool], // only search available this call
});

Parameters accept Zod v3.23+ and v4.x schemas. Both work identically:

// Zod v3
import { z } from "zod";
const params = z.object({ name: z.string() });
// Zod v4
import { z } from "zod/v4";
const params = z.object({ name: z.string() });

The SDK converts Zod schemas to JSON Schema internally via zodToJsonSchema().


type PermissionScope = "once" | "session" | "project" | "always";
ScopePersistenceUse case
onceSingle invocationDestructive operations
sessionCurrent agent sessionRepeated safe operations
projectPersisted to project directoryTeam-shared approvals
alwaysPersisted to user configTrusted tools

Generated when a tool with needsApproval: true is called, or by the backend itself.

interface PermissionRequest {
toolName: string;
toolArgs: Record<string, unknown>;
toolCallId?: string;
suggestedScope?: PermissionScope;
rawSDKRequest?: unknown;
}

Returned by the permission callback.

interface PermissionDecision {
allowed: boolean;
scope?: PermissionScope;
modifiedInput?: Record<string, unknown>;
reason?: string;
}

modifiedInput replaces the original tool arguments if provided. Use this to sanitize inputs.

Attach permission and user-input callbacks to an agent:

import type { SupervisorHooks, PermissionRequest, PermissionDecision } from "@witqq/agent-sdk";
const supervisor: SupervisorHooks = {
onPermission: async (request: PermissionRequest, signal: AbortSignal): Promise<PermissionDecision> => {
if (request.toolName === "delete_file") {
return { allowed: false, reason: "Destructive operations disabled" };
}
return { allowed: true, scope: "session" };
},
onAskUser: async (request, signal) => {
const answer = await promptUser(request.question, request.choices);
return { answer, wasFreeform: !request.choices };
},
};
const config: FullAgentConfig = {
systemPrompt: "Assistant with guardrails",
supervisor,
};

When needsApproval is true, the agent emits a permission_request event before executing:

const deployTool: ToolDefinition<{ target: string }> = {
name: "deploy",
description: "Deploy to production",
parameters: z.object({ target: z.string() }),
needsApproval: true,
execute: async (params) => {
return await deploy(params.target);
},
};

Without a supervisor.onPermission callback, the tool call is denied by default.

Permission stores persist approval decisions across calls.

interface IPermissionStore {
isApproved(toolName: string): Promise<boolean>;
approve(toolName: string, scope: PermissionScope): Promise<void>;
revoke(toolName: string): Promise<void>;
clear(): Promise<void>;
dispose(): Promise<void>;
}

Built-in implementations:

StoreImportBackingNotes
InMemoryPermissionStore@witqq/agent-sdkMap"once" scope not persisted
FilePermissionStore@witqq/agent-sdkJSON fileAtomic writes
CompositePermissionStore@witqq/agent-sdkMultiple storesRoutes by scope
import { InMemoryPermissionStore } from "@witqq/agent-sdk";
const store = new InMemoryPermissionStore();
await store.approve("search", "session");
await store.isApproved("search"); // true

Persists to a JSON file. Suitable for project-level permissions.

import { FilePermissionStore } from "@witqq/agent-sdk";
const store = new FilePermissionStore("/path/to/project/.permissions.json");
await store.approve("deploy", "project");

Routes approvals to different stores based on scope:

import {
CompositePermissionStore,
InMemoryPermissionStore,
FilePermissionStore,
} from "@witqq/agent-sdk";
const composite = new CompositePermissionStore(
new InMemoryPermissionStore(), // session scope
new FilePermissionStore("./project-perms.json"), // project scope
new FilePermissionStore("~/.config/agent/perms.json"), // always scope
);

Factory that creates a CompositePermissionStore with standard paths:

import { createDefaultPermissionStore } from "@witqq/agent-sdk";
const store = createDefaultPermissionStore("/path/to/project");
import {
ToolDefinition,
InMemoryPermissionStore,
type FullAgentConfig,
type PermissionRequest,
type PermissionDecision,
} from "@witqq/agent-sdk";
import { z } from "zod";
const readTool: ToolDefinition<{ path: string }> = {
name: "read_file",
description: "Read a file",
parameters: z.object({ path: z.string() }),
needsApproval: true,
execute: async ({ path }) => readFile(path, "utf-8"),
};
const deleteTool: ToolDefinition<{ path: string }> = {
name: "delete_file",
description: "Delete a file",
parameters: z.object({ path: z.string() }),
needsApproval: true,
execute: async ({ path }) => unlink(path),
};
const store = new InMemoryPermissionStore();
const config: FullAgentConfig = {
systemPrompt: "File assistant",
tools: [readTool, deleteTool],
permissionStore: store,
supervisor: {
onPermission: async (req: PermissionRequest, signal: AbortSignal): Promise<PermissionDecision> => {
// Check store first
if (await store.isApproved(req.toolName)) {
return { allowed: true };
}
// Auto-approve reads for the session
if (req.toolName === "read_file") {
await store.approve(req.toolName, "session");
return { allowed: true, scope: "session" };
}
// Deny deletes outside /tmp
const path = req.toolArgs.path as string;
if (req.toolName === "delete_file" && !path.startsWith("/tmp")) {
return { allowed: false, reason: "Can only delete files in /tmp" };
}
return { allowed: true, scope: "once" };
},
},
};

API Reference: Core Exports