fix: rename bash tool to exec (#748) (thanks @myfunc)
This commit is contained in:
@@ -84,7 +84,7 @@ describe("resolveAgentConfig", () => {
|
||||
workspace: "~/clawd-restricted",
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash", "write", "edit"],
|
||||
deny: ["exec", "write", "edit"],
|
||||
elevated: {
|
||||
enabled: false,
|
||||
allowFrom: { whatsapp: ["+15555550123"] },
|
||||
@@ -97,7 +97,7 @@ describe("resolveAgentConfig", () => {
|
||||
const result = resolveAgentConfig(cfg, "restricted");
|
||||
expect(result?.tools).toEqual({
|
||||
allow: ["read"],
|
||||
deny: ["bash", "write", "edit"],
|
||||
deny: ["exec", "write", "edit"],
|
||||
elevated: {
|
||||
enabled: false,
|
||||
allowFrom: { whatsapp: ["+15555550123"] },
|
||||
@@ -118,7 +118,7 @@ describe("resolveAgentConfig", () => {
|
||||
},
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash"],
|
||||
deny: ["exec"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
||||
import {
|
||||
bashTool,
|
||||
createBashTool,
|
||||
createExecTool,
|
||||
createProcessTool,
|
||||
execTool,
|
||||
processTool,
|
||||
} from "./bash-tools.js";
|
||||
import { sanitizeBinaryOutput } from "./shell-utils.js";
|
||||
@@ -50,7 +50,7 @@ beforeEach(() => {
|
||||
resetProcessRegistryForTests();
|
||||
});
|
||||
|
||||
describe("bash tool backgrounding", () => {
|
||||
describe("exec tool backgrounding", () => {
|
||||
const originalShell = process.env.SHELL;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -64,7 +64,7 @@ describe("bash tool backgrounding", () => {
|
||||
it(
|
||||
"backgrounds after yield and can be polled",
|
||||
async () => {
|
||||
const result = await bashTool.execute("call1", {
|
||||
const result = await execTool.execute("call1", {
|
||||
command: joinCommands([yieldDelayCmd, "echo done"]),
|
||||
yieldMs: 10,
|
||||
});
|
||||
@@ -97,7 +97,7 @@ describe("bash tool backgrounding", () => {
|
||||
);
|
||||
|
||||
it("supports explicit background", async () => {
|
||||
const result = await bashTool.execute("call1", {
|
||||
const result = await execTool.execute("call1", {
|
||||
command: echoAfterDelay("later"),
|
||||
background: true,
|
||||
});
|
||||
@@ -113,7 +113,7 @@ describe("bash tool backgrounding", () => {
|
||||
});
|
||||
|
||||
it("derives a session name from the command", async () => {
|
||||
const result = await bashTool.execute("call1", {
|
||||
const result = await execTool.execute("call1", {
|
||||
command: "echo hello",
|
||||
background: true,
|
||||
});
|
||||
@@ -129,7 +129,7 @@ describe("bash tool backgrounding", () => {
|
||||
});
|
||||
|
||||
it("uses default timeout when timeout is omitted", async () => {
|
||||
const customBash = createBashTool({ timeoutSec: 1, backgroundMs: 10 });
|
||||
const customBash = createExecTool({ timeoutSec: 1, backgroundMs: 10 });
|
||||
const customProcess = createProcessTool();
|
||||
|
||||
const result = await customBash.execute("call1", {
|
||||
@@ -156,7 +156,7 @@ describe("bash tool backgrounding", () => {
|
||||
});
|
||||
|
||||
it("rejects elevated requests when not allowed", async () => {
|
||||
const customBash = createBashTool({
|
||||
const customBash = createExecTool({
|
||||
elevated: { enabled: true, allowed: false, defaultLevel: "off" },
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("bash tool backgrounding", () => {
|
||||
});
|
||||
|
||||
it("does not default to elevated when not allowed", async () => {
|
||||
const customBash = createBashTool({
|
||||
const customBash = createExecTool({
|
||||
elevated: { enabled: true, allowed: false, defaultLevel: "on" },
|
||||
backgroundMs: 1000,
|
||||
timeoutSec: 5,
|
||||
@@ -183,7 +183,7 @@ describe("bash tool backgrounding", () => {
|
||||
});
|
||||
|
||||
it("logs line-based slices and defaults to last lines", async () => {
|
||||
const result = await bashTool.execute("call1", {
|
||||
const result = await execTool.execute("call1", {
|
||||
command: echoLines(["one", "two", "three"]),
|
||||
background: true,
|
||||
});
|
||||
@@ -203,7 +203,7 @@ describe("bash tool backgrounding", () => {
|
||||
});
|
||||
|
||||
it("supports line offsets for log slices", async () => {
|
||||
const result = await bashTool.execute("call1", {
|
||||
const result = await execTool.execute("call1", {
|
||||
command: echoLines(["alpha", "beta", "gamma"]),
|
||||
background: true,
|
||||
});
|
||||
@@ -221,9 +221,9 @@ describe("bash tool backgrounding", () => {
|
||||
});
|
||||
|
||||
it("scopes process sessions by scopeKey", async () => {
|
||||
const bashA = createBashTool({ backgroundMs: 10, scopeKey: "agent:alpha" });
|
||||
const bashA = createExecTool({ backgroundMs: 10, scopeKey: "agent:alpha" });
|
||||
const processA = createProcessTool({ scopeKey: "agent:alpha" });
|
||||
const bashB = createBashTool({ backgroundMs: 10, scopeKey: "agent:beta" });
|
||||
const bashB = createExecTool({ backgroundMs: 10, scopeKey: "agent:beta" });
|
||||
const processB = createProcessTool({ scopeKey: "agent:beta" });
|
||||
|
||||
const resultA = await bashA.execute("call1", {
|
||||
|
||||
@@ -54,11 +54,11 @@ const _stringEnum = <T extends readonly string[]>(
|
||||
...options,
|
||||
});
|
||||
|
||||
export type BashToolDefaults = {
|
||||
export type ExecToolDefaults = {
|
||||
backgroundMs?: number;
|
||||
timeoutSec?: number;
|
||||
sandbox?: BashSandboxConfig;
|
||||
elevated?: BashElevatedDefaults;
|
||||
elevated?: ExecElevatedDefaults;
|
||||
allowBackground?: boolean;
|
||||
scopeKey?: string;
|
||||
cwd?: string;
|
||||
@@ -76,14 +76,14 @@ export type BashSandboxConfig = {
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type BashElevatedDefaults = {
|
||||
export type ExecElevatedDefaults = {
|
||||
enabled: boolean;
|
||||
allowed: boolean;
|
||||
defaultLevel: "on" | "off";
|
||||
};
|
||||
|
||||
const bashSchema = Type.Object({
|
||||
command: Type.String({ description: "Bash command to execute" }),
|
||||
const execSchema = Type.Object({
|
||||
command: Type.String({ description: "Shell command to execute" }),
|
||||
workdir: Type.Optional(
|
||||
Type.String({ description: "Working directory (defaults to cwd)" }),
|
||||
),
|
||||
@@ -108,7 +108,7 @@ const bashSchema = Type.Object({
|
||||
),
|
||||
});
|
||||
|
||||
export type BashToolDetails =
|
||||
export type ExecToolDetails =
|
||||
| {
|
||||
status: "running";
|
||||
sessionId: string;
|
||||
@@ -125,10 +125,10 @@ export type BashToolDetails =
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
export function createBashTool(
|
||||
defaults?: BashToolDefaults,
|
||||
export function createExecTool(
|
||||
defaults?: ExecToolDefaults,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
|
||||
): AgentTool<any, BashToolDetails> {
|
||||
): AgentTool<any, ExecToolDetails> {
|
||||
const defaultBackgroundMs = clampNumber(
|
||||
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
|
||||
10_000,
|
||||
@@ -142,11 +142,11 @@ export function createBashTool(
|
||||
: 1800;
|
||||
|
||||
return {
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
name: "exec",
|
||||
label: "exec",
|
||||
description:
|
||||
"Execute bash with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.",
|
||||
parameters: bashSchema,
|
||||
"Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.",
|
||||
parameters: execSchema,
|
||||
execute: async (_toolCallId, args, signal, onUpdate) => {
|
||||
const params = args as {
|
||||
command: string;
|
||||
@@ -218,7 +218,7 @@ export function createBashTool(
|
||||
);
|
||||
}
|
||||
logInfo(
|
||||
`bash: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle(
|
||||
`exec: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle(
|
||||
params.command,
|
||||
120,
|
||||
)}`,
|
||||
@@ -363,7 +363,7 @@ export function createBashTool(
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise<AgentToolResult<BashToolDetails>>(
|
||||
return new Promise<AgentToolResult<ExecToolDetails>>(
|
||||
(resolve, reject) => {
|
||||
const resolveRunning = () => {
|
||||
settle(() =>
|
||||
@@ -482,7 +482,7 @@ export function createBashTool(
|
||||
};
|
||||
}
|
||||
|
||||
export const bashTool = createBashTool();
|
||||
export const execTool = createExecTool();
|
||||
|
||||
const processSchema = Type.Object({
|
||||
action: Type.String({ description: "Process action" }),
|
||||
@@ -509,7 +509,7 @@ export function createProcessTool(
|
||||
return {
|
||||
name: "process",
|
||||
label: "process",
|
||||
description: "Manage running bash sessions: list, poll, log, write, kill.",
|
||||
description: "Manage running exec sessions: list, poll, log, write, kill.",
|
||||
parameters: processSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as {
|
||||
|
||||
@@ -356,7 +356,7 @@ describe("sanitizeGoogleTurnOrdering", () => {
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "toolCall", id: "call_1", name: "bash", arguments: {} },
|
||||
{ type: "toolCall", id: "call_1", name: "exec", arguments: {} },
|
||||
],
|
||||
},
|
||||
] satisfies AgentMessage[];
|
||||
@@ -403,7 +403,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_abc|item:456",
|
||||
name: "bash",
|
||||
name: "exec",
|
||||
arguments: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("buildEmbeddedSandboxInfo", () => {
|
||||
env: { LANG: "C.UTF-8" },
|
||||
},
|
||||
tools: {
|
||||
allow: ["bash"],
|
||||
allow: ["exec"],
|
||||
deny: ["browser"],
|
||||
},
|
||||
browserAllowHostControl: true,
|
||||
@@ -87,7 +87,7 @@ describe("buildEmbeddedSandboxInfo", () => {
|
||||
env: { LANG: "C.UTF-8" },
|
||||
},
|
||||
tools: {
|
||||
allow: ["bash"],
|
||||
allow: ["exec"],
|
||||
deny: ["browser"],
|
||||
},
|
||||
browserAllowHostControl: false,
|
||||
@@ -171,7 +171,7 @@ function createStubTool(name: string): AgentTool {
|
||||
describe("splitSdkTools", () => {
|
||||
const tools = [
|
||||
createStubTool("read"),
|
||||
createStubTool("bash"),
|
||||
createStubTool("exec"),
|
||||
createStubTool("edit"),
|
||||
createStubTool("write"),
|
||||
createStubTool("browser"),
|
||||
@@ -185,7 +185,7 @@ describe("splitSdkTools", () => {
|
||||
expect(builtInTools).toEqual([]);
|
||||
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||
"read",
|
||||
"bash",
|
||||
"exec",
|
||||
"edit",
|
||||
"write",
|
||||
"browser",
|
||||
@@ -200,7 +200,7 @@ describe("splitSdkTools", () => {
|
||||
expect(builtInTools).toEqual([]);
|
||||
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||
"read",
|
||||
"bash",
|
||||
"exec",
|
||||
"edit",
|
||||
"write",
|
||||
"browser",
|
||||
@@ -226,7 +226,7 @@ describe("applyGoogleTurnOrderingFix", () => {
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "toolCall", id: "call_1", name: "bash", arguments: {} },
|
||||
{ type: "toolCall", id: "call_1", name: "exec", arguments: {} },
|
||||
],
|
||||
},
|
||||
] satisfies AgentMessage[];
|
||||
@@ -360,7 +360,7 @@ describe("limitHistoryTurns", () => {
|
||||
{ role: "user", content: [{ type: "text", text: "first" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "1", name: "bash", arguments: {} }],
|
||||
content: [{ type: "toolCall", id: "1", name: "exec", arguments: {} }],
|
||||
},
|
||||
{ role: "user", content: [{ type: "text", text: "second" }] },
|
||||
{ role: "assistant", content: [{ type: "text", text: "response" }] },
|
||||
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
markAuthProfileGood,
|
||||
markAuthProfileUsed,
|
||||
} from "./auth-profiles.js";
|
||||
import type { BashElevatedDefaults } from "./bash-tools.js";
|
||||
import type { ExecElevatedDefaults, ExecToolDefaults } from "./bash-tools.js";
|
||||
import {
|
||||
CONTEXT_WINDOW_HARD_MIN_TOKENS,
|
||||
CONTEXT_WINDOW_WARN_BELOW_TOKENS,
|
||||
@@ -768,11 +768,11 @@ function describeUnknownError(error: unknown): string {
|
||||
|
||||
export function buildEmbeddedSandboxInfo(
|
||||
sandbox?: Awaited<ReturnType<typeof resolveSandboxContext>>,
|
||||
bashElevated?: BashElevatedDefaults,
|
||||
execElevated?: ExecElevatedDefaults,
|
||||
): EmbeddedSandboxInfo | undefined {
|
||||
if (!sandbox?.enabled) return undefined;
|
||||
const elevatedAllowed = Boolean(
|
||||
bashElevated?.enabled && bashElevated.allowed,
|
||||
execElevated?.enabled && execElevated.allowed,
|
||||
);
|
||||
return {
|
||||
enabled: true,
|
||||
@@ -790,7 +790,7 @@ export function buildEmbeddedSandboxInfo(
|
||||
? {
|
||||
elevated: {
|
||||
allowed: true,
|
||||
defaultLevel: bashElevated?.defaultLevel ?? "off",
|
||||
defaultLevel: execElevated?.defaultLevel ?? "off",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@@ -949,6 +949,16 @@ function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {
|
||||
return level;
|
||||
}
|
||||
|
||||
function resolveExecToolDefaults(
|
||||
config?: ClawdbotConfig,
|
||||
): ExecToolDefaults | undefined {
|
||||
const tools = config?.tools;
|
||||
if (!tools) return undefined;
|
||||
if (!tools.exec) return tools.bash;
|
||||
if (!tools.bash) return tools.exec;
|
||||
return { ...tools.bash, ...tools.exec };
|
||||
}
|
||||
|
||||
function resolveModel(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
@@ -987,7 +997,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
model?: string;
|
||||
thinkLevel?: ThinkLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
bashElevated?: BashElevatedDefaults;
|
||||
bashElevated?: ExecElevatedDefaults;
|
||||
customInstructions?: string;
|
||||
lane?: string;
|
||||
enqueue?: typeof enqueueCommand;
|
||||
@@ -1087,8 +1097,8 @@ export async function compactEmbeddedPiSession(params: {
|
||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||
const runAbortController = new AbortController();
|
||||
const tools = createClawdbotCodingTools({
|
||||
bash: {
|
||||
...params.config?.tools?.bash,
|
||||
exec: {
|
||||
...resolveExecToolDefaults(params.config),
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox,
|
||||
@@ -1289,7 +1299,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||
thinkLevel?: ThinkLevel;
|
||||
verboseLevel?: VerboseLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
bashElevated?: BashElevatedDefaults;
|
||||
bashElevated?: ExecElevatedDefaults;
|
||||
timeoutMs: number;
|
||||
runId: string;
|
||||
abortSignal?: AbortSignal;
|
||||
@@ -1499,8 +1509,8 @@ export async function runEmbeddedPiAgent(params: {
|
||||
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
||||
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
||||
const tools = createClawdbotCodingTools({
|
||||
bash: {
|
||||
...params.config?.tools?.bash,
|
||||
exec: {
|
||||
...resolveExecToolDefaults(params.config),
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
sandbox,
|
||||
|
||||
@@ -94,28 +94,28 @@ describe("context-pruning", () => {
|
||||
makeAssistant("a1"),
|
||||
makeToolResult({
|
||||
toolCallId: "t1",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "x".repeat(20_000),
|
||||
}),
|
||||
makeUser("u2"),
|
||||
makeAssistant("a2"),
|
||||
makeToolResult({
|
||||
toolCallId: "t2",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "y".repeat(20_000),
|
||||
}),
|
||||
makeUser("u3"),
|
||||
makeAssistant("a3"),
|
||||
makeToolResult({
|
||||
toolCallId: "t3",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "z".repeat(20_000),
|
||||
}),
|
||||
makeUser("u4"),
|
||||
makeAssistant("a4"),
|
||||
makeToolResult({
|
||||
toolCallId: "t4",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "w".repeat(20_000),
|
||||
}),
|
||||
];
|
||||
@@ -161,7 +161,7 @@ describe("context-pruning", () => {
|
||||
makeUser("u1"),
|
||||
makeToolResult({
|
||||
toolCallId: "t1",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "y".repeat(20_000),
|
||||
}),
|
||||
];
|
||||
@@ -184,19 +184,19 @@ describe("context-pruning", () => {
|
||||
makeAssistant("a1"),
|
||||
makeToolResult({
|
||||
toolCallId: "t1",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "x".repeat(20_000),
|
||||
}),
|
||||
makeToolResult({
|
||||
toolCallId: "t2",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "y".repeat(20_000),
|
||||
}),
|
||||
makeUser("u2"),
|
||||
makeAssistant("a2"),
|
||||
makeToolResult({
|
||||
toolCallId: "t3",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "z".repeat(20_000),
|
||||
}),
|
||||
];
|
||||
@@ -225,7 +225,7 @@ describe("context-pruning", () => {
|
||||
makeAssistant("a1"),
|
||||
makeToolResult({
|
||||
toolCallId: "t1",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "x".repeat(20_000),
|
||||
}),
|
||||
makeAssistant("a2"),
|
||||
@@ -273,7 +273,7 @@ describe("context-pruning", () => {
|
||||
makeAssistant("a1"),
|
||||
makeToolResult({
|
||||
toolCallId: "t1",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "x".repeat(20_000),
|
||||
}),
|
||||
makeAssistant("a2"),
|
||||
@@ -313,7 +313,7 @@ describe("context-pruning", () => {
|
||||
makeUser("u1"),
|
||||
makeToolResult({
|
||||
toolCallId: "t1",
|
||||
toolName: "Bash",
|
||||
toolName: "Exec",
|
||||
text: "x".repeat(20_000),
|
||||
}),
|
||||
makeToolResult({
|
||||
@@ -329,7 +329,7 @@ describe("context-pruning", () => {
|
||||
softTrimRatio: 0.0,
|
||||
hardClearRatio: 0.0,
|
||||
minPrunableToolChars: 0,
|
||||
tools: { allow: ["ba*"], deny: ["bash"] },
|
||||
tools: { allow: ["ex*"], deny: ["exec"] },
|
||||
hardClear: { enabled: true, placeholder: "[cleared]" },
|
||||
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
|
||||
};
|
||||
@@ -339,7 +339,7 @@ describe("context-pruning", () => {
|
||||
} as unknown as ExtensionContext;
|
||||
const next = pruneContextMessages({ messages, settings, ctx });
|
||||
|
||||
// Deny wins => bash is not pruned, even though allow matches.
|
||||
// Deny wins => exec is not pruned, even though allow matches.
|
||||
expect(toolText(findToolResult(next, "t1"))).toContain("x".repeat(20_000));
|
||||
// allow is non-empty and browser is not allowed => never pruned.
|
||||
expect(toolText(findToolResult(next, "t2"))).toContain("y".repeat(20_000));
|
||||
@@ -350,7 +350,7 @@ describe("context-pruning", () => {
|
||||
makeUser("u1"),
|
||||
makeImageToolResult({
|
||||
toolCallId: "t1",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "x".repeat(20_000),
|
||||
}),
|
||||
];
|
||||
@@ -384,7 +384,7 @@ describe("context-pruning", () => {
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
content: [
|
||||
{ type: "text", text: "AAAAA" },
|
||||
{ type: "text", text: "BBBBB" },
|
||||
@@ -418,7 +418,7 @@ describe("context-pruning", () => {
|
||||
makeUser("u1"),
|
||||
makeToolResult({
|
||||
toolCallId: "t1",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
text: "abcdefghij".repeat(1000),
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
const toolNames = tools.map((t) => t.name);
|
||||
expect(toolNames).toContain("read");
|
||||
expect(toolNames).toContain("write");
|
||||
expect(toolNames).not.toContain("bash");
|
||||
expect(toolNames).not.toContain("exec");
|
||||
});
|
||||
|
||||
it("should keep global tool policy when agent only sets tools.elevated", () => {
|
||||
@@ -62,7 +62,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
});
|
||||
|
||||
const toolNames = tools.map((t) => t.name);
|
||||
expect(toolNames).toContain("bash");
|
||||
expect(toolNames).toContain("exec");
|
||||
expect(toolNames).toContain("read");
|
||||
expect(toolNames).not.toContain("write");
|
||||
});
|
||||
@@ -70,7 +70,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
it("should apply agent-specific tool policy", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: {
|
||||
allow: ["read", "write", "bash"],
|
||||
allow: ["read", "write", "exec"],
|
||||
deny: [],
|
||||
},
|
||||
agents: {
|
||||
@@ -80,7 +80,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
workspace: "~/clawd-restricted",
|
||||
tools: {
|
||||
allow: ["read"], // Agent override: only read
|
||||
deny: ["bash", "write", "edit"],
|
||||
deny: ["exec", "write", "edit"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -96,7 +96,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
|
||||
const toolNames = tools.map((t) => t.name);
|
||||
expect(toolNames).toContain("read");
|
||||
expect(toolNames).not.toContain("bash");
|
||||
expect(toolNames).not.toContain("exec");
|
||||
expect(toolNames).not.toContain("write");
|
||||
expect(toolNames).not.toContain("edit");
|
||||
});
|
||||
@@ -115,7 +115,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
workspace: "~/clawd-family",
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash", "write", "edit", "process"],
|
||||
deny: ["exec", "write", "edit", "process"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -130,7 +130,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
agentDir: "/tmp/agent-main",
|
||||
});
|
||||
const mainToolNames = mainTools.map((t) => t.name);
|
||||
expect(mainToolNames).toContain("bash");
|
||||
expect(mainToolNames).toContain("exec");
|
||||
expect(mainToolNames).toContain("write");
|
||||
expect(mainToolNames).toContain("edit");
|
||||
|
||||
@@ -143,7 +143,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
});
|
||||
const familyToolNames = familyTools.map((t) => t.name);
|
||||
expect(familyToolNames).toContain("read");
|
||||
expect(familyToolNames).not.toContain("bash");
|
||||
expect(familyToolNames).not.toContain("exec");
|
||||
expect(familyToolNames).not.toContain("write");
|
||||
expect(familyToolNames).not.toContain("edit");
|
||||
});
|
||||
@@ -159,7 +159,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
id: "work",
|
||||
workspace: "~/clawd-work",
|
||||
tools: {
|
||||
deny: ["bash", "process"], // Agent deny (override)
|
||||
deny: ["exec", "process"], // Agent deny (override)
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -176,7 +176,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
const toolNames = tools.map((t) => t.name);
|
||||
// Agent policy overrides global: browser is allowed again
|
||||
expect(toolNames).toContain("browser");
|
||||
expect(toolNames).not.toContain("bash");
|
||||
expect(toolNames).not.toContain("exec");
|
||||
expect(toolNames).not.toContain("process");
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
},
|
||||
tools: {
|
||||
allow: ["read"], // Agent further restricts to only read
|
||||
deny: ["bash", "write"],
|
||||
deny: ["exec", "write"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -207,7 +207,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
tools: {
|
||||
sandbox: {
|
||||
tools: {
|
||||
allow: ["read", "write", "bash"], // Sandbox allows these
|
||||
allow: ["read", "write", "exec"], // Sandbox allows these
|
||||
deny: [],
|
||||
},
|
||||
},
|
||||
@@ -237,7 +237,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
capDrop: [],
|
||||
} satisfies SandboxDockerConfig,
|
||||
tools: {
|
||||
allow: ["read", "write", "bash"],
|
||||
allow: ["read", "write", "exec"],
|
||||
deny: [],
|
||||
},
|
||||
browserAllowHostControl: false,
|
||||
@@ -246,14 +246,14 @@ describe("Agent-specific tool filtering", () => {
|
||||
|
||||
const toolNames = tools.map((t) => t.name);
|
||||
// Agent policy should be applied first, then sandbox
|
||||
// Agent allows only "read", sandbox allows ["read", "write", "bash"]
|
||||
// Agent allows only "read", sandbox allows ["read", "write", "exec"]
|
||||
// Result: only "read" (most restrictive wins)
|
||||
expect(toolNames).toContain("read");
|
||||
expect(toolNames).not.toContain("bash");
|
||||
expect(toolNames).not.toContain("exec");
|
||||
expect(toolNames).not.toContain("write");
|
||||
});
|
||||
|
||||
it("should run bash synchronously when process is denied", async () => {
|
||||
it("should run exec synchronously when process is denied", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: {
|
||||
deny: ["process"],
|
||||
@@ -266,10 +266,10 @@ describe("Agent-specific tool filtering", () => {
|
||||
workspaceDir: "/tmp/test-main",
|
||||
agentDir: "/tmp/agent-main",
|
||||
});
|
||||
const bash = tools.find((tool) => tool.name === "bash");
|
||||
expect(bash).toBeDefined();
|
||||
const execTool = tools.find((tool) => tool.name === "exec");
|
||||
expect(execTool).toBeDefined();
|
||||
|
||||
const result = await bash?.execute("call1", {
|
||||
const result = await execTool?.execute("call1", {
|
||||
command: "echo done",
|
||||
yieldMs: 10,
|
||||
});
|
||||
|
||||
@@ -153,9 +153,9 @@ describe("createClawdbotCodingTools", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("includes bash and process tools", () => {
|
||||
it("includes exec and process tools", () => {
|
||||
const tools = createClawdbotCodingTools();
|
||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
||||
expect(tools.some((tool) => tool.name === "exec")).toBe(true);
|
||||
expect(tools.some((tool) => tool.name === "process")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -165,7 +165,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
modelAuthMode: "oauth",
|
||||
});
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
expect(names.has("bash")).toBe(true);
|
||||
expect(names.has("exec")).toBe(true);
|
||||
expect(names.has("read")).toBe(true);
|
||||
expect(names.has("write")).toBe(true);
|
||||
expect(names.has("edit")).toBe(true);
|
||||
@@ -210,7 +210,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
expect(names.has("sessions_spawn")).toBe(false);
|
||||
|
||||
expect(names.has("read")).toBe(true);
|
||||
expect(names.has("bash")).toBe(true);
|
||||
expect(names.has("exec")).toBe(true);
|
||||
expect(names.has("process")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -330,7 +330,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
browserAllowHostControl: false,
|
||||
};
|
||||
const tools = createClawdbotCodingTools({ sandbox });
|
||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
||||
expect(tools.some((tool) => tool.name === "exec")).toBe(true);
|
||||
expect(tools.some((tool) => tool.name === "read")).toBe(false);
|
||||
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||
});
|
||||
@@ -371,7 +371,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
const tools = createClawdbotCodingTools({
|
||||
config: { tools: { deny: ["browser"] } },
|
||||
});
|
||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
||||
expect(tools.some((tool) => tool.name === "exec")).toBe(true);
|
||||
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "./agent-scope.js";
|
||||
import {
|
||||
type BashToolDefaults,
|
||||
createBashTool,
|
||||
createExecTool,
|
||||
createProcessTool,
|
||||
type ExecToolDefaults,
|
||||
type ProcessToolDefaults,
|
||||
} from "./bash-tools.js";
|
||||
import { createClawdbotTools } from "./clawdbot-tools.js";
|
||||
@@ -290,9 +290,18 @@ function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||
return cleanSchemaForGemini(schema);
|
||||
}
|
||||
|
||||
const TOOL_NAME_ALIASES: Record<string, string> = {
|
||||
bash: "exec",
|
||||
};
|
||||
|
||||
function normalizeToolName(name: string) {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
return TOOL_NAME_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
function normalizeToolNames(list?: string[]) {
|
||||
if (!list) return [];
|
||||
return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
||||
return list.map(normalizeToolName).filter(Boolean);
|
||||
}
|
||||
|
||||
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||
@@ -354,7 +363,7 @@ function isToolAllowedByPolicy(name: string, policy?: SandboxToolPolicy) {
|
||||
const deny = new Set(normalizeToolNames(policy.deny));
|
||||
const allowRaw = normalizeToolNames(policy.allow);
|
||||
const allow = allowRaw.length > 0 ? new Set(allowRaw) : null;
|
||||
const normalized = name.trim().toLowerCase();
|
||||
const normalized = normalizeToolName(name);
|
||||
if (deny.has(normalized)) return false;
|
||||
if (allow) return allow.has(normalized);
|
||||
return true;
|
||||
@@ -467,7 +476,7 @@ function wrapToolWithAbortSignal(
|
||||
}
|
||||
|
||||
export function createClawdbotCodingTools(options?: {
|
||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
||||
exec?: ExecToolDefaults & ProcessToolDefaults;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
sandbox?: SandboxContext | null;
|
||||
@@ -495,14 +504,14 @@ export function createClawdbotCodingTools(options?: {
|
||||
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
||||
hasRepliedRef?: { value: boolean };
|
||||
}): AnyAgentTool[] {
|
||||
const bashToolName = "bash";
|
||||
const execToolName = "exec";
|
||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||
const { agentId, policy: effectiveToolsPolicy } = resolveEffectiveToolPolicy({
|
||||
config: options?.config,
|
||||
sessionKey: options?.sessionKey,
|
||||
});
|
||||
const scopeKey =
|
||||
options?.bash?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||
options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||
const subagentPolicy =
|
||||
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
||||
? resolveSubagentToolPolicy(options.config)
|
||||
@@ -524,7 +533,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
const freshReadTool = createReadTool(workspaceRoot);
|
||||
return [createClawdbotReadTool(freshReadTool)];
|
||||
}
|
||||
if (tool.name === bashToolName) return [];
|
||||
if (tool.name === "bash" || tool.name === execToolName) return [];
|
||||
if (tool.name === "write") {
|
||||
if (sandboxRoot) return [];
|
||||
return [createWriteTool(workspaceRoot)];
|
||||
@@ -535,8 +544,8 @@ export function createClawdbotCodingTools(options?: {
|
||||
}
|
||||
return [tool as AnyAgentTool];
|
||||
});
|
||||
const bashTool = createBashTool({
|
||||
...options?.bash,
|
||||
const execTool = createExecTool({
|
||||
...options?.exec,
|
||||
cwd: options?.workspaceDir,
|
||||
allowBackground,
|
||||
scopeKey,
|
||||
@@ -550,7 +559,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
: undefined,
|
||||
});
|
||||
const processTool = createProcessTool({
|
||||
cleanupMs: options?.bash?.cleanupMs,
|
||||
cleanupMs: options?.exec?.cleanupMs,
|
||||
scopeKey,
|
||||
});
|
||||
const tools: AnyAgentTool[] = [
|
||||
@@ -563,7 +572,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
]
|
||||
: []
|
||||
: []),
|
||||
bashTool as unknown as AnyAgentTool,
|
||||
execTool as unknown as AnyAgentTool,
|
||||
processTool as unknown as AnyAgentTool,
|
||||
// Provider docking: include provider-defined agent tools (login, etc.).
|
||||
...listProviderAgentTools({ cfg: options?.config }),
|
||||
|
||||
@@ -110,13 +110,13 @@ describe("workspace path resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults bash cwd to workspaceDir when workdir is omitted", async () => {
|
||||
it("defaults exec cwd to workspaceDir when workdir is omitted", async () => {
|
||||
await withTempDir("clawdbot-ws-", async (workspaceDir) => {
|
||||
const tools = createClawdbotCodingTools({ workspaceDir });
|
||||
const bashTool = tools.find((tool) => tool.name === "bash");
|
||||
expect(bashTool).toBeDefined();
|
||||
const execTool = tools.find((tool) => tool.name === "exec");
|
||||
expect(execTool).toBeDefined();
|
||||
|
||||
const result = await bashTool?.execute("ws-bash", {
|
||||
const result = await execTool?.execute("ws-exec", {
|
||||
command: "echo ok",
|
||||
});
|
||||
const cwd =
|
||||
@@ -134,14 +134,14 @@ describe("workspace path resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lets bash workdir override the workspace default", async () => {
|
||||
it("lets exec workdir override the workspace default", async () => {
|
||||
await withTempDir("clawdbot-ws-", async (workspaceDir) => {
|
||||
await withTempDir("clawdbot-override-", async (overrideDir) => {
|
||||
const tools = createClawdbotCodingTools({ workspaceDir });
|
||||
const bashTool = tools.find((tool) => tool.name === "bash");
|
||||
expect(bashTool).toBeDefined();
|
||||
const execTool = tools.find((tool) => tool.name === "exec");
|
||||
expect(execTool).toBeDefined();
|
||||
|
||||
const result = await bashTool?.execute("ws-bash-override", {
|
||||
const result = await execTool?.execute("ws-exec-override", {
|
||||
command: "echo ok",
|
||||
workdir: overrideDir,
|
||||
});
|
||||
|
||||
@@ -450,7 +450,7 @@ describe("Agent-specific sandbox config", () => {
|
||||
sandbox: {
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
deny: ["bash"],
|
||||
deny: ["exec"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -163,7 +163,7 @@ const DEFAULT_SANDBOX_WORKDIR = "/workspace";
|
||||
const DEFAULT_SANDBOX_IDLE_HOURS = 24;
|
||||
const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7;
|
||||
const DEFAULT_TOOL_ALLOW = [
|
||||
"bash",
|
||||
"exec",
|
||||
"process",
|
||||
"read",
|
||||
"write",
|
||||
|
||||
@@ -9,14 +9,14 @@ describe("sanitizeToolUseResultPairing", () => {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "toolCall", id: "call_1", name: "read", arguments: {} },
|
||||
{ type: "toolCall", id: "call_2", name: "bash", arguments: {} },
|
||||
{ type: "toolCall", id: "call_2", name: "exec", arguments: {} },
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "user message that should come after tool use" },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_2",
|
||||
toolName: "bash",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
isError: false,
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||
it("lists available tools when provided", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/clawd",
|
||||
toolNames: ["bash", "sessions_list", "sessions_history", "sessions_send"],
|
||||
toolNames: ["exec", "sessions_list", "sessions_history", "sessions_send"],
|
||||
});
|
||||
|
||||
expect(prompt).toContain("Tool availability (filtered by policy):");
|
||||
@@ -49,13 +49,13 @@ describe("buildAgentSystemPrompt", () => {
|
||||
it("preserves tool casing in the prompt", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/clawd",
|
||||
toolNames: ["Read", "Bash", "process"],
|
||||
toolNames: ["Read", "Exec", "process"],
|
||||
skillsPrompt:
|
||||
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
|
||||
});
|
||||
|
||||
expect(prompt).toContain("- Read: Read file contents");
|
||||
expect(prompt).toContain("- Bash: Run shell commands");
|
||||
expect(prompt).toContain("- Exec: Run shell commands");
|
||||
expect(prompt).toContain(
|
||||
"Use `Read` to load the SKILL.md at the location listed for that skill.",
|
||||
);
|
||||
@@ -90,7 +90,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||
it("adds ClaudeBot self-update guidance when gateway tool is available", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/clawd",
|
||||
toolNames: ["gateway", "bash"],
|
||||
toolNames: ["gateway", "exec"],
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Clawdbot Self-Update");
|
||||
|
||||
@@ -53,8 +53,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
grep: "Search file contents for patterns",
|
||||
find: "Find files by glob pattern",
|
||||
ls: "List directory contents",
|
||||
bash: "Run shell commands",
|
||||
process: "Manage background bash sessions",
|
||||
exec: "Run shell commands",
|
||||
process: "Manage background exec sessions",
|
||||
// Provider docking: add provider login tools here when a provider needs interactive linking.
|
||||
browser: "Control web browser",
|
||||
canvas: "Present/eval/snapshot the Canvas",
|
||||
@@ -80,7 +80,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
"grep",
|
||||
"find",
|
||||
"ls",
|
||||
"bash",
|
||||
"exec",
|
||||
"process",
|
||||
"browser",
|
||||
"canvas",
|
||||
@@ -133,7 +133,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
|
||||
const hasGateway = availableTools.has("gateway");
|
||||
const readToolName = resolveToolName("read");
|
||||
const bashToolName = resolveToolName("bash");
|
||||
const execToolName = resolveToolName("exec");
|
||||
const processToolName = resolveToolName("process");
|
||||
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
||||
const ownerNumbers = (params.ownerNumbers ?? [])
|
||||
@@ -195,8 +195,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
"- grep: search file contents for patterns",
|
||||
"- find: find files by glob pattern",
|
||||
"- ls: list directory contents",
|
||||
`- ${bashToolName}: run shell commands (supports background via yieldMs/background)`,
|
||||
`- ${processToolName}: manage background bash sessions`,
|
||||
`- ${execToolName}: run shell commands (supports background via yieldMs/background)`,
|
||||
`- ${processToolName}: manage background exec sessions`,
|
||||
"- browser: control clawd's dedicated browser",
|
||||
"- canvas: present/eval/snapshot the Canvas",
|
||||
"- nodes: list/describe/notify/camera/screen on paired nodes",
|
||||
@@ -277,7 +277,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
)}`
|
||||
: "",
|
||||
params.sandboxInfo.elevated?.allowed
|
||||
? "Elevated bash is available for this session."
|
||||
? "Elevated exec is available for this session."
|
||||
: "",
|
||||
params.sandboxInfo.elevated?.allowed
|
||||
? "User can toggle with /elevated on|off."
|
||||
@@ -288,7 +288,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
params.sandboxInfo.elevated?.allowed
|
||||
? `Current elevated level: ${
|
||||
params.sandboxInfo.elevated.defaultLevel
|
||||
} (on runs bash on host; off runs in sandbox).`
|
||||
} (on runs exec on host; off runs in sandbox).`
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -315,7 +315,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
"## Messaging",
|
||||
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)",
|
||||
"- Cross-session messaging → use sessions_send(sessionKey, message)",
|
||||
"- Never use bash/curl for provider messaging; Clawdbot handles all routing internally.",
|
||||
"- Never use exec/curl for provider messaging; Clawdbot handles all routing internally.",
|
||||
availableTools.has("message")
|
||||
? [
|
||||
"",
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"bash": {
|
||||
"exec": {
|
||||
"emoji": "🛠️",
|
||||
"title": "Bash",
|
||||
"title": "Exec",
|
||||
"detailKeys": ["command"]
|
||||
},
|
||||
"process": {
|
||||
|
||||
Reference in New Issue
Block a user