mirror of https://github.com/openclaw/openclaw.git
552 lines
21 KiB
TypeScript
552 lines
21 KiB
TypeScript
import { codingTools, createReadTool, readTool } from "@mariozechner/pi-coding-agent";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import type { ToolLoopDetectionConfig } from "../config/types.tools.js";
|
|
import { resolveMergedSafeBinProfileFixtures } from "../infra/exec-safe-bin-runtime-policy.js";
|
|
import { logWarn } from "../logger.js";
|
|
import { getPluginToolMeta } from "../plugins/tools.js";
|
|
import { isSubagentSessionKey } from "../routing/session-key.js";
|
|
import { resolveGatewayMessageChannel } from "../utils/message-channel.js";
|
|
import { resolveAgentConfig } from "./agent-scope.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 { resolveImageSanitizationLimits } from "./image-sanitization.js";
|
|
import type { ModelAuthMode } from "./model-auth.js";
|
|
import { createOpenClawTools } from "./openclaw-tools.js";
|
|
import { wrapToolWithAbortSignal } from "./pi-tools.abort.js";
|
|
import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
|
|
import {
|
|
isToolAllowedByPolicies,
|
|
resolveEffectiveToolPolicy,
|
|
resolveGroupToolPolicy,
|
|
resolveSubagentToolPolicy,
|
|
} from "./pi-tools.policy.js";
|
|
import {
|
|
assertRequiredParams,
|
|
createHostWorkspaceEditTool,
|
|
createHostWorkspaceWriteTool,
|
|
createOpenClawReadTool,
|
|
createSandboxedEditTool,
|
|
createSandboxedReadTool,
|
|
createSandboxedWriteTool,
|
|
normalizeToolParams,
|
|
patchToolSchemaForClaudeCompatibility,
|
|
wrapToolWorkspaceRootGuard,
|
|
wrapToolWorkspaceRootGuardWithOptions,
|
|
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 { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
|
import { createToolFsPolicy, resolveToolFsConfig } from "./tool-fs-policy.js";
|
|
import {
|
|
applyToolPolicyPipeline,
|
|
buildDefaultToolPolicyPipelineSteps,
|
|
} from "./tool-policy-pipeline.js";
|
|
import {
|
|
applyOwnerOnlyToolPolicy,
|
|
collectExplicitAllowlist,
|
|
mergeAlsoAllowPolicy,
|
|
resolveToolProfilePolicy,
|
|
} from "./tool-policy.js";
|
|
import { resolveWorkspaceRoot } from "./workspace-dir.js";
|
|
|
|
function isOpenAIProvider(provider?: string) {
|
|
const normalized = provider?.trim().toLowerCase();
|
|
return normalized === "openai" || normalized === "openai-codex";
|
|
}
|
|
|
|
const TOOL_DENY_BY_MESSAGE_PROVIDER: Readonly<Record<string, readonly string[]>> = {
|
|
voice: ["tts"],
|
|
};
|
|
|
|
function normalizeMessageProvider(messageProvider?: string): string | undefined {
|
|
const normalized = messageProvider?.trim().toLowerCase();
|
|
return normalized && normalized.length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
function applyMessageProviderToolPolicy(
|
|
tools: AnyAgentTool[],
|
|
messageProvider?: string,
|
|
): AnyAgentTool[] {
|
|
const normalizedProvider = normalizeMessageProvider(messageProvider);
|
|
if (!normalizedProvider) {
|
|
return tools;
|
|
}
|
|
const deniedTools = TOOL_DENY_BY_MESSAGE_PROVIDER[normalizedProvider];
|
|
if (!deniedTools || deniedTools.length === 0) {
|
|
return tools;
|
|
}
|
|
const deniedSet = new Set(deniedTools);
|
|
return tools.filter((tool) => !deniedSet.has(tool.name));
|
|
}
|
|
|
|
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;
|
|
});
|
|
}
|
|
|
|
function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
|
|
const cfg = params.cfg;
|
|
const globalExec = cfg?.tools?.exec;
|
|
const agentExec =
|
|
cfg && params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools?.exec : undefined;
|
|
return {
|
|
host: agentExec?.host ?? globalExec?.host,
|
|
security: agentExec?.security ?? globalExec?.security,
|
|
ask: agentExec?.ask ?? globalExec?.ask,
|
|
node: agentExec?.node ?? globalExec?.node,
|
|
pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend,
|
|
safeBins: agentExec?.safeBins ?? globalExec?.safeBins,
|
|
safeBinTrustedDirs: agentExec?.safeBinTrustedDirs ?? globalExec?.safeBinTrustedDirs,
|
|
safeBinProfiles: resolveMergedSafeBinProfileFixtures({
|
|
global: globalExec,
|
|
local: agentExec,
|
|
}),
|
|
backgroundMs: agentExec?.backgroundMs ?? globalExec?.backgroundMs,
|
|
timeoutSec: agentExec?.timeoutSec ?? globalExec?.timeoutSec,
|
|
approvalRunningNoticeMs:
|
|
agentExec?.approvalRunningNoticeMs ?? globalExec?.approvalRunningNoticeMs,
|
|
cleanupMs: agentExec?.cleanupMs ?? globalExec?.cleanupMs,
|
|
notifyOnExit: agentExec?.notifyOnExit ?? globalExec?.notifyOnExit,
|
|
notifyOnExitEmptySuccess:
|
|
agentExec?.notifyOnExitEmptySuccess ?? globalExec?.notifyOnExitEmptySuccess,
|
|
applyPatch: agentExec?.applyPatch ?? globalExec?.applyPatch,
|
|
};
|
|
}
|
|
|
|
export function resolveToolLoopDetectionConfig(params: {
|
|
cfg?: OpenClawConfig;
|
|
agentId?: string;
|
|
}): ToolLoopDetectionConfig | undefined {
|
|
const global = params.cfg?.tools?.loopDetection;
|
|
const agent =
|
|
params.agentId && params.cfg
|
|
? resolveAgentConfig(params.cfg, params.agentId)?.tools?.loopDetection
|
|
: undefined;
|
|
|
|
if (!agent) {
|
|
return global;
|
|
}
|
|
if (!global) {
|
|
return agent;
|
|
}
|
|
|
|
return {
|
|
...global,
|
|
...agent,
|
|
detectors: {
|
|
...global.detectors,
|
|
...agent.detectors,
|
|
},
|
|
};
|
|
}
|
|
|
|
export const __testing = {
|
|
cleanToolSchemaForGemini,
|
|
normalizeToolParams,
|
|
patchToolSchemaForClaudeCompatibility,
|
|
wrapToolParamNormalization,
|
|
assertRequiredParams,
|
|
} as const;
|
|
|
|
export function createOpenClawCodingTools(options?: {
|
|
agentId?: string;
|
|
exec?: ExecToolDefaults & ProcessToolDefaults;
|
|
messageProvider?: string;
|
|
agentAccountId?: string;
|
|
messageTo?: string;
|
|
messageThreadId?: string | number;
|
|
sandbox?: SandboxContext | null;
|
|
sessionKey?: string;
|
|
/** Ephemeral session UUID — regenerated on /new and /reset. */
|
|
sessionId?: string;
|
|
agentDir?: string;
|
|
workspaceDir?: string;
|
|
config?: OpenClawConfig;
|
|
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;
|
|
/** Model context window in tokens (used to scale read-tool output budget). */
|
|
modelContextWindowTokens?: number;
|
|
/**
|
|
* 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;
|
|
/** Current inbound message id for action fallbacks (e.g. Telegram react). */
|
|
currentMessageId?: string | number;
|
|
/** Group id for channel-level tool policy resolution. */
|
|
groupId?: string | null;
|
|
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
groupChannel?: string | null;
|
|
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
groupSpace?: string | null;
|
|
/** Parent session key for subagent group policy inheritance. */
|
|
spawnedBy?: string | null;
|
|
senderId?: string | null;
|
|
senderName?: string | null;
|
|
senderUsername?: string | null;
|
|
senderE164?: string | null;
|
|
/** 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 };
|
|
/** If true, the model has native vision capability */
|
|
modelHasVision?: boolean;
|
|
/** Require explicit message targets (no implicit last-route sends). */
|
|
requireExplicitMessageTarget?: boolean;
|
|
/** If true, omit the message tool from the tool list. */
|
|
disableMessageTool?: boolean;
|
|
/** Whether the sender is an owner (required for owner-only tools). */
|
|
senderIsOwner?: boolean;
|
|
}): AnyAgentTool[] {
|
|
const execToolName = "exec";
|
|
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
|
const {
|
|
agentId,
|
|
globalPolicy,
|
|
globalProviderPolicy,
|
|
agentPolicy,
|
|
agentProviderPolicy,
|
|
profile,
|
|
providerProfile,
|
|
profileAlsoAllow,
|
|
providerProfileAlsoAllow,
|
|
} = resolveEffectiveToolPolicy({
|
|
config: options?.config,
|
|
sessionKey: options?.sessionKey,
|
|
agentId: options?.agentId,
|
|
modelProvider: options?.modelProvider,
|
|
modelId: options?.modelId,
|
|
});
|
|
const groupPolicy = resolveGroupToolPolicy({
|
|
config: options?.config,
|
|
sessionKey: options?.sessionKey,
|
|
spawnedBy: options?.spawnedBy,
|
|
messageProvider: options?.messageProvider,
|
|
groupId: options?.groupId,
|
|
groupChannel: options?.groupChannel,
|
|
groupSpace: options?.groupSpace,
|
|
accountId: options?.agentAccountId,
|
|
senderId: options?.senderId,
|
|
senderName: options?.senderName,
|
|
senderUsername: options?.senderUsername,
|
|
senderE164: options?.senderE164,
|
|
});
|
|
const profilePolicy = resolveToolProfilePolicy(profile);
|
|
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
|
|
|
const profilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(profilePolicy, profileAlsoAllow);
|
|
const providerProfilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(
|
|
providerProfilePolicy,
|
|
providerProfileAlsoAllow,
|
|
);
|
|
// Prefer sessionKey for process isolation scope to prevent cross-session process visibility/killing.
|
|
// Fallback to agentId if no sessionKey is available (e.g. legacy or global contexts).
|
|
const scopeKey =
|
|
options?.exec?.scopeKey ?? options?.sessionKey ?? (agentId ? `agent:${agentId}` : undefined);
|
|
const subagentPolicy =
|
|
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
|
? resolveSubagentToolPolicy(
|
|
options.config,
|
|
getSubagentDepthFromSessionStore(options.sessionKey, { cfg: options.config }),
|
|
)
|
|
: undefined;
|
|
const allowBackground = isToolAllowedByPolicies("process", [
|
|
profilePolicyWithAlsoAllow,
|
|
providerProfilePolicyWithAlsoAllow,
|
|
globalPolicy,
|
|
globalProviderPolicy,
|
|
agentPolicy,
|
|
agentProviderPolicy,
|
|
groupPolicy,
|
|
sandbox?.tools,
|
|
subagentPolicy,
|
|
]);
|
|
const execConfig = resolveExecConfig({ cfg: options?.config, agentId });
|
|
const fsConfig = resolveToolFsConfig({ cfg: options?.config, agentId });
|
|
const fsPolicy = createToolFsPolicy({
|
|
workspaceOnly: fsConfig.workspaceOnly,
|
|
});
|
|
const sandboxRoot = sandbox?.workspaceDir;
|
|
const sandboxFsBridge = sandbox?.fsBridge;
|
|
const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";
|
|
const workspaceRoot = resolveWorkspaceRoot(options?.workspaceDir);
|
|
const workspaceOnly = fsPolicy.workspaceOnly;
|
|
const applyPatchConfig = execConfig.applyPatch;
|
|
// Secure by default: apply_patch is workspace-contained unless explicitly disabled.
|
|
// (tools.fs.workspaceOnly is a separate umbrella flag for read/write/edit/apply_patch.)
|
|
const applyPatchWorkspaceOnly = workspaceOnly || applyPatchConfig?.workspaceOnly !== false;
|
|
const applyPatchEnabled =
|
|
!!applyPatchConfig?.enabled &&
|
|
isOpenAIProvider(options?.modelProvider) &&
|
|
isApplyPatchAllowedForModel({
|
|
modelProvider: options?.modelProvider,
|
|
modelId: options?.modelId,
|
|
allowModels: applyPatchConfig?.allowModels,
|
|
});
|
|
|
|
if (sandboxRoot && !sandboxFsBridge) {
|
|
throw new Error("Sandbox filesystem bridge is unavailable.");
|
|
}
|
|
const imageSanitization = resolveImageSanitizationLimits(options?.config);
|
|
|
|
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
|
if (tool.name === readTool.name) {
|
|
if (sandboxRoot) {
|
|
const sandboxed = createSandboxedReadTool({
|
|
root: sandboxRoot,
|
|
bridge: sandboxFsBridge!,
|
|
modelContextWindowTokens: options?.modelContextWindowTokens,
|
|
imageSanitization,
|
|
});
|
|
return [
|
|
workspaceOnly
|
|
? wrapToolWorkspaceRootGuardWithOptions(sandboxed, sandboxRoot, {
|
|
containerWorkdir: sandbox.containerWorkdir,
|
|
})
|
|
: sandboxed,
|
|
];
|
|
}
|
|
const freshReadTool = createReadTool(workspaceRoot);
|
|
const wrapped = createOpenClawReadTool(freshReadTool, {
|
|
modelContextWindowTokens: options?.modelContextWindowTokens,
|
|
imageSanitization,
|
|
});
|
|
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
|
|
}
|
|
if (tool.name === "bash" || tool.name === execToolName) {
|
|
return [];
|
|
}
|
|
if (tool.name === "write") {
|
|
if (sandboxRoot) {
|
|
return [];
|
|
}
|
|
const wrapped = createHostWorkspaceWriteTool(workspaceRoot, { workspaceOnly });
|
|
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
|
|
}
|
|
if (tool.name === "edit") {
|
|
if (sandboxRoot) {
|
|
return [];
|
|
}
|
|
const wrapped = createHostWorkspaceEditTool(workspaceRoot, { workspaceOnly });
|
|
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
|
|
}
|
|
return [tool];
|
|
});
|
|
const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {};
|
|
const execTool = createExecTool({
|
|
...execDefaults,
|
|
host: options?.exec?.host ?? execConfig.host,
|
|
security: options?.exec?.security ?? execConfig.security,
|
|
ask: options?.exec?.ask ?? execConfig.ask,
|
|
node: options?.exec?.node ?? execConfig.node,
|
|
pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend,
|
|
safeBins: options?.exec?.safeBins ?? execConfig.safeBins,
|
|
safeBinTrustedDirs: options?.exec?.safeBinTrustedDirs ?? execConfig.safeBinTrustedDirs,
|
|
safeBinProfiles: options?.exec?.safeBinProfiles ?? execConfig.safeBinProfiles,
|
|
agentId,
|
|
cwd: workspaceRoot,
|
|
allowBackground,
|
|
scopeKey,
|
|
sessionKey: options?.sessionKey,
|
|
messageProvider: options?.messageProvider,
|
|
currentChannelId: options?.currentChannelId,
|
|
currentThreadTs: options?.currentThreadTs,
|
|
accountId: options?.agentAccountId,
|
|
backgroundMs: options?.exec?.backgroundMs ?? execConfig.backgroundMs,
|
|
timeoutSec: options?.exec?.timeoutSec ?? execConfig.timeoutSec,
|
|
approvalRunningNoticeMs:
|
|
options?.exec?.approvalRunningNoticeMs ?? execConfig.approvalRunningNoticeMs,
|
|
notifyOnExit: options?.exec?.notifyOnExit ?? execConfig.notifyOnExit,
|
|
notifyOnExitEmptySuccess:
|
|
options?.exec?.notifyOnExitEmptySuccess ?? execConfig.notifyOnExitEmptySuccess,
|
|
sandbox: sandbox
|
|
? {
|
|
containerName: sandbox.containerName,
|
|
workspaceDir: sandbox.workspaceDir,
|
|
containerWorkdir: sandbox.containerWorkdir,
|
|
env: sandbox.docker.env,
|
|
}
|
|
: undefined,
|
|
});
|
|
const processTool = createProcessTool({
|
|
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
|
|
scopeKey,
|
|
});
|
|
const applyPatchTool =
|
|
!applyPatchEnabled || (sandboxRoot && !allowWorkspaceWrites)
|
|
? null
|
|
: createApplyPatchTool({
|
|
cwd: sandboxRoot ?? workspaceRoot,
|
|
sandbox:
|
|
sandboxRoot && allowWorkspaceWrites
|
|
? { root: sandboxRoot, bridge: sandboxFsBridge! }
|
|
: undefined,
|
|
workspaceOnly: applyPatchWorkspaceOnly,
|
|
});
|
|
const tools: AnyAgentTool[] = [
|
|
...base,
|
|
...(sandboxRoot
|
|
? allowWorkspaceWrites
|
|
? [
|
|
workspaceOnly
|
|
? wrapToolWorkspaceRootGuardWithOptions(
|
|
createSandboxedEditTool({ root: sandboxRoot, bridge: sandboxFsBridge! }),
|
|
sandboxRoot,
|
|
{
|
|
containerWorkdir: sandbox.containerWorkdir,
|
|
},
|
|
)
|
|
: createSandboxedEditTool({ root: sandboxRoot, bridge: sandboxFsBridge! }),
|
|
workspaceOnly
|
|
? wrapToolWorkspaceRootGuardWithOptions(
|
|
createSandboxedWriteTool({ root: sandboxRoot, bridge: sandboxFsBridge! }),
|
|
sandboxRoot,
|
|
{
|
|
containerWorkdir: sandbox.containerWorkdir,
|
|
},
|
|
)
|
|
: createSandboxedWriteTool({ root: sandboxRoot, bridge: sandboxFsBridge! }),
|
|
]
|
|
: []
|
|
: []),
|
|
...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
|
|
execTool as unknown as AnyAgentTool,
|
|
processTool as unknown as AnyAgentTool,
|
|
// Channel docking: include channel-defined agent tools (login, etc.).
|
|
...listChannelAgentTools({ cfg: options?.config }),
|
|
...createOpenClawTools({
|
|
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
|
|
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
|
|
agentSessionKey: options?.sessionKey,
|
|
agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
|
|
agentAccountId: options?.agentAccountId,
|
|
agentTo: options?.messageTo,
|
|
agentThreadId: options?.messageThreadId,
|
|
agentGroupId: options?.groupId ?? null,
|
|
agentGroupChannel: options?.groupChannel ?? null,
|
|
agentGroupSpace: options?.groupSpace ?? null,
|
|
agentDir: options?.agentDir,
|
|
sandboxRoot,
|
|
sandboxFsBridge,
|
|
fsPolicy,
|
|
workspaceDir: workspaceRoot,
|
|
sandboxed: !!sandbox,
|
|
config: options?.config,
|
|
pluginToolAllowlist: collectExplicitAllowlist([
|
|
profilePolicy,
|
|
providerProfilePolicy,
|
|
globalPolicy,
|
|
globalProviderPolicy,
|
|
agentPolicy,
|
|
agentProviderPolicy,
|
|
groupPolicy,
|
|
sandbox?.tools,
|
|
subagentPolicy,
|
|
]),
|
|
currentChannelId: options?.currentChannelId,
|
|
currentThreadTs: options?.currentThreadTs,
|
|
currentMessageId: options?.currentMessageId,
|
|
replyToMode: options?.replyToMode,
|
|
hasRepliedRef: options?.hasRepliedRef,
|
|
modelHasVision: options?.modelHasVision,
|
|
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
|
|
disableMessageTool: options?.disableMessageTool,
|
|
requesterAgentIdOverride: agentId,
|
|
requesterSenderId: options?.senderId,
|
|
senderIsOwner: options?.senderIsOwner,
|
|
sessionId: options?.sessionId,
|
|
}),
|
|
];
|
|
const toolsForMessageProvider = applyMessageProviderToolPolicy(tools, options?.messageProvider);
|
|
// Security: treat unknown/undefined as unauthorized (opt-in, not opt-out)
|
|
const senderIsOwner = options?.senderIsOwner === true;
|
|
const toolsByAuthorization = applyOwnerOnlyToolPolicy(toolsForMessageProvider, senderIsOwner);
|
|
const subagentFiltered = applyToolPolicyPipeline({
|
|
tools: toolsByAuthorization,
|
|
toolMeta: (tool) => getPluginToolMeta(tool),
|
|
warn: logWarn,
|
|
steps: [
|
|
...buildDefaultToolPolicyPipelineSteps({
|
|
profilePolicy: profilePolicyWithAlsoAllow,
|
|
profile,
|
|
providerProfilePolicy: providerProfilePolicyWithAlsoAllow,
|
|
providerProfile,
|
|
globalPolicy,
|
|
globalProviderPolicy,
|
|
agentPolicy,
|
|
agentProviderPolicy,
|
|
groupPolicy,
|
|
agentId,
|
|
}),
|
|
{ policy: sandbox?.tools, label: "sandbox tools.allow" },
|
|
{ policy: subagentPolicy, label: "subagent tools.allow" },
|
|
],
|
|
});
|
|
// 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.
|
|
// Provider-specific cleaning: Gemini needs constraint keywords stripped, but Anthropic expects them.
|
|
const normalized = subagentFiltered.map((tool) =>
|
|
normalizeToolParameters(tool, {
|
|
modelProvider: options?.modelProvider,
|
|
modelId: options?.modelId,
|
|
}),
|
|
);
|
|
const withHooks = normalized.map((tool) =>
|
|
wrapToolWithBeforeToolCallHook(tool, {
|
|
agentId,
|
|
sessionKey: options?.sessionKey,
|
|
sessionId: options?.sessionId,
|
|
loopDetection: resolveToolLoopDetectionConfig({ cfg: options?.config, agentId }),
|
|
}),
|
|
);
|
|
const withAbort = options?.abortSignal
|
|
? withHooks.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal))
|
|
: withHooks;
|
|
|
|
// 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;
|
|
}
|