fix: honor sandboxed built-in tools
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
|
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||||
|
import { Type } from "@sinclair/typebox";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
import { buildEmbeddedSandboxInfo } from "./pi-embedded-runner.js";
|
buildEmbeddedSandboxInfo,
|
||||||
|
splitSdkTools,
|
||||||
|
} from "./pi-embedded-runner.js";
|
||||||
import type { SandboxContext } from "./sandbox.js";
|
import type { SandboxContext } from "./sandbox.js";
|
||||||
|
|
||||||
describe("buildEmbeddedSandboxInfo", () => {
|
describe("buildEmbeddedSandboxInfo", () => {
|
||||||
@@ -45,3 +49,52 @@ describe("buildEmbeddedSandboxInfo", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function createStubTool(name: string): AgentTool {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
label: name,
|
||||||
|
description: "",
|
||||||
|
parameters: Type.Object({}),
|
||||||
|
execute: async () => ({ content: [], details: {} }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("splitSdkTools", () => {
|
||||||
|
const tools = [
|
||||||
|
createStubTool("read"),
|
||||||
|
createStubTool("bash"),
|
||||||
|
createStubTool("edit"),
|
||||||
|
createStubTool("write"),
|
||||||
|
createStubTool("browser"),
|
||||||
|
];
|
||||||
|
|
||||||
|
it("routes built-ins to custom tools when sandboxed", () => {
|
||||||
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
|
tools,
|
||||||
|
sandboxEnabled: true,
|
||||||
|
});
|
||||||
|
expect(builtInTools).toEqual([]);
|
||||||
|
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||||
|
"read",
|
||||||
|
"bash",
|
||||||
|
"edit",
|
||||||
|
"write",
|
||||||
|
"browser",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps built-ins as SDK tools when not sandboxed", () => {
|
||||||
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
|
tools,
|
||||||
|
sandboxEnabled: false,
|
||||||
|
});
|
||||||
|
expect(builtInTools.map((tool) => tool.name)).toEqual([
|
||||||
|
"read",
|
||||||
|
"bash",
|
||||||
|
"edit",
|
||||||
|
"write",
|
||||||
|
]);
|
||||||
|
expect(customTools.map((tool) => tool.name)).toEqual(["browser"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
import type { AgentMessage, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
import type {
|
||||||
|
AgentMessage,
|
||||||
|
AgentTool,
|
||||||
|
ThinkingLevel,
|
||||||
|
} from "@mariozechner/pi-agent-core";
|
||||||
import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai";
|
import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai";
|
||||||
import {
|
import {
|
||||||
buildSystemPrompt,
|
buildSystemPrompt,
|
||||||
@@ -12,6 +16,7 @@ import {
|
|||||||
SettingsManager,
|
SettingsManager,
|
||||||
type Skill,
|
type Skill,
|
||||||
} from "@mariozechner/pi-coding-agent";
|
} from "@mariozechner/pi-coding-agent";
|
||||||
|
import type { TSchema } from "@sinclair/typebox";
|
||||||
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
||||||
import type {
|
import type {
|
||||||
ReasoningLevel,
|
ReasoningLevel,
|
||||||
@@ -249,6 +254,33 @@ export function buildEmbeddedSandboxInfo(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]);
|
||||||
|
|
||||||
|
type AnyAgentTool = AgentTool<TSchema, unknown>;
|
||||||
|
|
||||||
|
export function splitSdkTools(options: {
|
||||||
|
tools: AnyAgentTool[];
|
||||||
|
sandboxEnabled: boolean;
|
||||||
|
}): {
|
||||||
|
builtInTools: AnyAgentTool[];
|
||||||
|
customTools: ReturnType<typeof toToolDefinitions>;
|
||||||
|
} {
|
||||||
|
// SDK rebuilds built-ins from cwd; route sandboxed versions as custom tools.
|
||||||
|
const { tools, sandboxEnabled } = options;
|
||||||
|
if (sandboxEnabled) {
|
||||||
|
return {
|
||||||
|
builtInTools: [],
|
||||||
|
customTools: toToolDefinitions(tools),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
builtInTools: tools.filter((tool) => BUILT_IN_TOOL_NAMES.has(tool.name)),
|
||||||
|
customTools: toToolDefinitions(
|
||||||
|
tools.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool.name)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function queueEmbeddedPiMessage(
|
export function queueEmbeddedPiMessage(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
text: string,
|
text: string,
|
||||||
@@ -528,11 +560,10 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
agentDir,
|
agentDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
const builtInToolNames = new Set(["read", "bash", "edit", "write"]);
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
const builtInTools = tools.filter((t) => builtInToolNames.has(t.name));
|
tools,
|
||||||
const customTools = toToolDefinitions(
|
sandboxEnabled: !!sandbox?.enabled,
|
||||||
tools.filter((t) => !builtInToolNames.has(t.name)),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const { session } = await createAgentSession({
|
const { session } = await createAgentSession({
|
||||||
cwd: resolvedWorkspace,
|
cwd: resolvedWorkspace,
|
||||||
@@ -829,14 +860,10 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
agentDir,
|
agentDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Split tools into built-in (recognized by pi-coding-agent SDK) and custom (clawdbot-specific)
|
const { builtInTools, customTools } = splitSdkTools({
|
||||||
const builtInToolNames = new Set(["read", "bash", "edit", "write"]);
|
tools,
|
||||||
const builtInTools = tools.filter((t) =>
|
sandboxEnabled: !!sandbox?.enabled,
|
||||||
builtInToolNames.has(t.name),
|
});
|
||||||
);
|
|
||||||
const customTools = toToolDefinitions(
|
|
||||||
tools.filter((t) => !builtInToolNames.has(t.name)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { session } = await createAgentSession({
|
const { session } = await createAgentSession({
|
||||||
cwd: resolvedWorkspace,
|
cwd: resolvedWorkspace,
|
||||||
|
|||||||
@@ -162,7 +162,9 @@ describe("subscribeEmbeddedPiSession", () => {
|
|||||||
|
|
||||||
expect(onBlockReply).toHaveBeenCalledTimes(1);
|
expect(onBlockReply).toHaveBeenCalledTimes(1);
|
||||||
const payload = onBlockReply.mock.calls[0][0];
|
const payload = onBlockReply.mock.calls[0][0];
|
||||||
expect(payload.text).toBe("_Reasoning:_\n_Because it helps_\n\nFinal answer");
|
expect(payload.text).toBe(
|
||||||
|
"_Reasoning:_\n_Because it helps_\n\nFinal answer",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits block replies on text_end and does not duplicate on message_end", () => {
|
it("emits block replies on text_end and does not duplicate on message_end", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user