import { codingTools, createEditTool, createReadTool, createWriteTool, readTool, } from "@mariozechner/pi-coding-agent"; import type { ClawdbotConfig } from "../config/config.js"; import { isSubagentSessionKey } from "../routing/session-key.js"; import { resolveGatewayMessageChannel } from "../utils/message-channel.js"; import { createApplyPatchTool } from "./apply-patch.js"; import { createExecTool, createProcessTool, type ExecToolDefaults, type ProcessToolDefaults, } from "./bash-tools.js"; import { listChannelAgentTools } from "./channel-tools.js"; import { createClawdbotTools } from "./clawdbot-tools.js"; import type { ModelAuthMode } from "./model-auth.js"; import { wrapToolWithAbortSignal } from "./pi-tools.abort.js"; import { filterToolsByPolicy, isToolAllowedByPolicies, resolveEffectiveToolPolicy, resolveSubagentToolPolicy, } from "./pi-tools.policy.js"; import { assertRequiredParams, CLAUDE_PARAM_GROUPS, createClawdbotReadTool, createSandboxedEditTool, createSandboxedReadTool, createSandboxedWriteTool, normalizeToolParams, patchToolSchemaForClaudeCompatibility, wrapToolParamNormalization, } from "./pi-tools.read.js"; import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js"; import type { AnyAgentTool } from "./pi-tools.types.js"; import type { SandboxContext } from "./sandbox.js"; import { resolveToolProfilePolicy } from "./tool-policy.js"; function isOpenAIProvider(provider?: string) { const normalized = provider?.trim().toLowerCase(); return normalized === "openai" || normalized === "openai-codex"; } function isApplyPatchAllowedForModel(params: { modelProvider?: string; modelId?: string; allowModels?: string[]; }) { const allowModels = Array.isArray(params.allowModels) ? params.allowModels : []; if (allowModels.length === 0) return true; const modelId = params.modelId?.trim(); if (!modelId) return false; const normalizedModelId = modelId.toLowerCase(); const provider = params.modelProvider?.trim().toLowerCase(); const normalizedFull = provider && !normalizedModelId.includes("/") ? `${provider}/${normalizedModelId}` : normalizedModelId; return allowModels.some((entry) => { const normalized = entry.trim().toLowerCase(); if (!normalized) return false; return normalized === normalizedModelId || normalized === normalizedFull; }); } export const __testing = { cleanToolSchemaForGemini, normalizeToolParams, patchToolSchemaForClaudeCompatibility, wrapToolParamNormalization, assertRequiredParams, } as const; export function createClawdbotCodingTools(options?: { exec?: ExecToolDefaults & ProcessToolDefaults; messageProvider?: string; agentAccountId?: string; sandbox?: SandboxContext | null; sessionKey?: string; agentDir?: string; workspaceDir?: string; config?: ClawdbotConfig; abortSignal?: AbortSignal; /** * Provider of the currently selected model (used for provider-specific tool quirks). * Example: "anthropic", "openai", "google", "openai-codex". */ modelProvider?: string; /** Model id for the current provider (used for model-specific tool gating). */ modelId?: string; /** * Auth mode for the current provider. We only need this for Anthropic OAuth * tool-name blocking quirks. */ modelAuthMode?: ModelAuthMode; /** Current channel ID for auto-threading (Slack). */ currentChannelId?: string; /** Current thread timestamp for auto-threading (Slack). */ currentThreadTs?: string; /** Reply-to mode for Slack auto-threading. */ replyToMode?: "off" | "first" | "all"; /** Mutable ref to track if a reply was sent (for "first" mode). */ hasRepliedRef?: { value: boolean }; }): AnyAgentTool[] { const execToolName = "exec"; const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined; const { agentId, globalPolicy, globalProviderPolicy, agentPolicy, agentProviderPolicy, profile, providerProfile, } = resolveEffectiveToolPolicy({ config: options?.config, sessionKey: options?.sessionKey, modelProvider: options?.modelProvider, modelId: options?.modelId, }); const profilePolicy = resolveToolProfilePolicy(profile); const providerProfilePolicy = resolveToolProfilePolicy(providerProfile); const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); const subagentPolicy = isSubagentSessionKey(options?.sessionKey) && options?.sessionKey ? resolveSubagentToolPolicy(options.config) : undefined; const allowBackground = isToolAllowedByPolicies("process", [ profilePolicy, providerProfilePolicy, globalPolicy, globalProviderPolicy, agentPolicy, agentProviderPolicy, sandbox?.tools, subagentPolicy, ]); const sandboxRoot = sandbox?.workspaceDir; const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro"; const workspaceRoot = options?.workspaceDir ?? process.cwd(); const applyPatchConfig = options?.config?.tools?.exec?.applyPatch; const applyPatchEnabled = !!applyPatchConfig?.enabled && isOpenAIProvider(options?.modelProvider) && isApplyPatchAllowedForModel({ modelProvider: options?.modelProvider, modelId: options?.modelId, allowModels: applyPatchConfig?.allowModels, }); const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => { if (tool.name === readTool.name) { if (sandboxRoot) { return [createSandboxedReadTool(sandboxRoot)]; } const freshReadTool = createReadTool(workspaceRoot); return [createClawdbotReadTool(freshReadTool)]; } if (tool.name === "bash" || tool.name === execToolName) return []; if (tool.name === "write") { if (sandboxRoot) return []; // Wrap with param normalization for Claude Code compatibility return [ wrapToolParamNormalization(createWriteTool(workspaceRoot), CLAUDE_PARAM_GROUPS.write), ]; } if (tool.name === "edit") { if (sandboxRoot) return []; // Wrap with param normalization for Claude Code compatibility return [wrapToolParamNormalization(createEditTool(workspaceRoot), CLAUDE_PARAM_GROUPS.edit)]; } return [tool as AnyAgentTool]; }); const execTool = createExecTool({ ...options?.exec, cwd: options?.workspaceDir, allowBackground, scopeKey, sandbox: sandbox ? { containerName: sandbox.containerName, workspaceDir: sandbox.workspaceDir, containerWorkdir: sandbox.containerWorkdir, env: sandbox.docker.env, } : undefined, }); const bashTool = { ...(execTool as unknown as AnyAgentTool), name: "bash", label: "bash", } satisfies AnyAgentTool; const processTool = createProcessTool({ cleanupMs: options?.exec?.cleanupMs, scopeKey, }); const applyPatchTool = !applyPatchEnabled || (sandboxRoot && !allowWorkspaceWrites) ? null : createApplyPatchTool({ cwd: sandboxRoot ?? workspaceRoot, sandboxRoot: sandboxRoot && allowWorkspaceWrites ? sandboxRoot : undefined, }); const tools: AnyAgentTool[] = [ ...base, ...(sandboxRoot ? allowWorkspaceWrites ? [createSandboxedEditTool(sandboxRoot), createSandboxedWriteTool(sandboxRoot)] : [] : []), ...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []), execTool as unknown as AnyAgentTool, bashTool, processTool as unknown as AnyAgentTool, // Channel docking: include channel-defined agent tools (login, etc.). ...listChannelAgentTools({ cfg: options?.config }), ...createClawdbotTools({ browserControlUrl: sandbox?.browser?.controlUrl, allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true, allowedControlUrls: sandbox?.browserAllowedControlUrls, allowedControlHosts: sandbox?.browserAllowedControlHosts, allowedControlPorts: sandbox?.browserAllowedControlPorts, agentSessionKey: options?.sessionKey, agentChannel: resolveGatewayMessageChannel(options?.messageProvider), agentAccountId: options?.agentAccountId, agentDir: options?.agentDir, sandboxRoot, workspaceDir: options?.workspaceDir, sandboxed: !!sandbox, config: options?.config, currentChannelId: options?.currentChannelId, currentThreadTs: options?.currentThreadTs, replyToMode: options?.replyToMode, hasRepliedRef: options?.hasRepliedRef, }), ]; const toolsFiltered = profilePolicy ? filterToolsByPolicy(tools, profilePolicy) : tools; const providerProfileFiltered = providerProfilePolicy ? filterToolsByPolicy(toolsFiltered, providerProfilePolicy) : toolsFiltered; const globalFiltered = globalPolicy ? filterToolsByPolicy(providerProfileFiltered, globalPolicy) : providerProfileFiltered; const globalProviderFiltered = globalProviderPolicy ? filterToolsByPolicy(globalFiltered, globalProviderPolicy) : globalFiltered; const agentFiltered = agentPolicy ? filterToolsByPolicy(globalProviderFiltered, agentPolicy) : globalProviderFiltered; const agentProviderFiltered = agentProviderPolicy ? filterToolsByPolicy(agentFiltered, agentProviderPolicy) : agentFiltered; const sandboxed = sandbox ? filterToolsByPolicy(agentProviderFiltered, sandbox.tools) : agentProviderFiltered; const subagentFiltered = subagentPolicy ? filterToolsByPolicy(sandboxed, subagentPolicy) : sandboxed; // Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai. // Without this, some providers (notably OpenAI) will reject root-level union schemas. const normalized = subagentFiltered.map(normalizeToolParameters); const withAbort = options?.abortSignal ? normalized.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal)) : normalized; // NOTE: Keep canonical (lowercase) tool names here. // pi-ai's Anthropic OAuth transport remaps tool names to Claude Code-style names // on the wire and maps them back for tool dispatch. return withAbort; }