276 lines
9.9 KiB
TypeScript
276 lines
9.9 KiB
TypeScript
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;
|
|
}
|