fix: override agent tools + sync bash without process
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
- Discord: include all inbound attachments in `MediaPaths`/`MediaUrls` (back-compat `MediaPath`/`MediaUrl` still first).
|
- Discord: include all inbound attachments in `MediaPaths`/`MediaUrls` (back-compat `MediaPath`/`MediaUrl` still first).
|
||||||
- Sandbox: add `agent.sandbox.workspaceAccess` (`none`/`ro`/`rw`) to control agent workspace visibility inside the container; `ro` hard-disables `write`/`edit`.
|
- Sandbox: add `agent.sandbox.workspaceAccess` (`none`/`ro`/`rw`) to control agent workspace visibility inside the container; `ro` hard-disables `write`/`edit`.
|
||||||
- Routing: allow per-agent sandbox overrides (including `workspaceAccess` and `sandbox.tools`) plus per-agent tool policies in multi-agent configs. Thanks @pasogott for PR #380.
|
- Routing: allow per-agent sandbox overrides (including `workspaceAccess` and `sandbox.tools`) plus per-agent tool policies in multi-agent configs. Thanks @pasogott for PR #380.
|
||||||
|
- Tools: make per-agent tool policies override global defaults and run bash synchronously when `process` is disallowed.
|
||||||
- Cron: clamp timer delay to avoid TimeoutOverflowWarning. Thanks @emanuelst for PR #412.
|
- Cron: clamp timer delay to avoid TimeoutOverflowWarning. Thanks @emanuelst for PR #412.
|
||||||
- Web UI: allow reconnect + password URL auth for the control UI and always scrub auth params from the URL. Thanks @oswalpalash for PR #414.
|
- Web UI: allow reconnect + password URL auth for the control UI and always scrub auth params from the URL. Thanks @oswalpalash for PR #414.
|
||||||
- ClawdbotKit: fix SwiftPM resource bundling path for `tool-display.json`. Thanks @fcatuhe for PR #398.
|
- ClawdbotKit: fix SwiftPM resource bundling path for `tool-display.json`. Thanks @fcatuhe for PR #398.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ Behavior:
|
|||||||
- Foreground runs return output directly.
|
- Foreground runs return output directly.
|
||||||
- When backgrounded (explicit or timeout), the tool returns `status: "running"` + `sessionId` and a short tail.
|
- When backgrounded (explicit or timeout), the tool returns `status: "running"` + `sessionId` and a short tail.
|
||||||
- Output is kept in memory until the session is polled or cleared.
|
- Output is kept in memory until the session is polled or cleared.
|
||||||
|
- If the `process` tool is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`.
|
||||||
|
|
||||||
Environment overrides:
|
Environment overrides:
|
||||||
- `PI_BASH_YIELD_MS`: default yield (ms)
|
- `PI_BASH_YIELD_MS`: default yield (ms)
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside o
|
|||||||
- `scope`: `"session"` | `"agent"` | `"shared"`
|
- `scope`: `"session"` | `"agent"` | `"shared"`
|
||||||
- `workspaceRoot`: custom sandbox workspace root
|
- `workspaceRoot`: custom sandbox workspace root
|
||||||
- `tools`: per-agent sandbox tool policy (deny wins; overrides `agent.sandbox.tools`)
|
- `tools`: per-agent sandbox tool policy (deny wins; overrides `agent.sandbox.tools`)
|
||||||
- `tools`: per-agent tool restrictions (applied before sandbox tool policy).
|
- `tools`: per-agent tool restrictions (overrides `agent.tools`; applied before sandbox tool policy).
|
||||||
- `allow`: array of allowed tool names
|
- `allow`: array of allowed tool names
|
||||||
- `deny`: array of denied tool names (deny wins)
|
- `deny`: array of denied tool names (deny wins)
|
||||||
- `routing.bindings[]`: routes inbound messages to an `agentId`.
|
- `routing.bindings[]`: routes inbound messages to an `agentId`.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ read_when:
|
|||||||
# Bash tool
|
# Bash tool
|
||||||
|
|
||||||
Run shell commands in the workspace. Supports foreground + background execution via `process`.
|
Run shell commands in the workspace. Supports foreground + background execution via `process`.
|
||||||
|
If `process` is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`.
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ Core parameters:
|
|||||||
Notes:
|
Notes:
|
||||||
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
||||||
- Use `process` to poll/log/write/kill/clear background sessions.
|
- Use `process` to poll/log/write/kill/clear background sessions.
|
||||||
|
- If `process` is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`.
|
||||||
|
|
||||||
### `process`
|
### `process`
|
||||||
Manage background bash sessions.
|
Manage background bash sessions.
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export type BashToolDefaults = {
|
|||||||
timeoutSec?: number;
|
timeoutSec?: number;
|
||||||
sandbox?: BashSandboxConfig;
|
sandbox?: BashSandboxConfig;
|
||||||
elevated?: BashElevatedDefaults;
|
elevated?: BashElevatedDefaults;
|
||||||
|
allowBackground?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProcessToolDefaults = {
|
export type ProcessToolDefaults = {
|
||||||
@@ -128,6 +129,7 @@ export function createBashTool(
|
|||||||
10,
|
10,
|
||||||
120_000,
|
120_000,
|
||||||
);
|
);
|
||||||
|
const allowBackground = defaults?.allowBackground ?? true;
|
||||||
const defaultTimeoutSec =
|
const defaultTimeoutSec =
|
||||||
typeof defaults?.timeoutSec === "number" && defaults.timeoutSec > 0
|
typeof defaults?.timeoutSec === "number" && defaults.timeoutSec > 0
|
||||||
? defaults.timeoutSec
|
? defaults.timeoutSec
|
||||||
@@ -154,14 +156,23 @@ export function createBashTool(
|
|||||||
throw new Error("Provide a command to start.");
|
throw new Error("Provide a command to start.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const yieldWindow = params.background
|
const backgroundRequested = params.background === true;
|
||||||
? 0
|
const yieldRequested = typeof params.yieldMs === "number";
|
||||||
: clampNumber(
|
if (!allowBackground && (backgroundRequested || yieldRequested)) {
|
||||||
params.yieldMs ?? defaultBackgroundMs,
|
warnings.push(
|
||||||
defaultBackgroundMs,
|
"Warning: background execution is disabled; running synchronously.",
|
||||||
10,
|
);
|
||||||
120_000,
|
}
|
||||||
);
|
const yieldWindow = allowBackground
|
||||||
|
? backgroundRequested
|
||||||
|
? 0
|
||||||
|
: clampNumber(
|
||||||
|
params.yieldMs ?? defaultBackgroundMs,
|
||||||
|
defaultBackgroundMs,
|
||||||
|
10,
|
||||||
|
120_000,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
const maxOutput = DEFAULT_MAX_OUTPUT;
|
const maxOutput = DEFAULT_MAX_OUTPUT;
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const sessionId = randomUUID();
|
const sessionId = randomUUID();
|
||||||
@@ -353,15 +364,17 @@ export function createBashTool(
|
|||||||
resolveRunning();
|
resolveRunning();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (yieldWindow === 0) {
|
if (allowBackground && yieldWindow !== null) {
|
||||||
onYieldNow();
|
if (yieldWindow === 0) {
|
||||||
} else {
|
onYieldNow();
|
||||||
yieldTimer = setTimeout(() => {
|
} else {
|
||||||
if (settled) return;
|
yieldTimer = setTimeout(() => {
|
||||||
yielded = true;
|
if (settled) return;
|
||||||
markBackgrounded(session);
|
yielded = true;
|
||||||
resolveRunning();
|
markBackgrounded(session);
|
||||||
}, yieldWindow);
|
resolveRunning();
|
||||||
|
}, yieldWindow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExit = (
|
const handleExit = (
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ describe("Agent-specific tool filtering", () => {
|
|||||||
expect(familyToolNames).not.toContain("edit");
|
expect(familyToolNames).not.toContain("edit");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should combine global and agent-specific deny lists", () => {
|
it("should prefer agent-specific tool policy over global", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agent: {
|
||||||
tools: {
|
tools: {
|
||||||
@@ -126,7 +126,7 @@ describe("Agent-specific tool filtering", () => {
|
|||||||
work: {
|
work: {
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
tools: {
|
tools: {
|
||||||
deny: ["bash", "process"], // Agent deny
|
deny: ["bash", "process"], // Agent deny (override)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -141,8 +141,8 @@ describe("Agent-specific tool filtering", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const toolNames = tools.map((t) => t.name);
|
const toolNames = tools.map((t) => t.name);
|
||||||
// Both global and agent denies should be applied
|
// Agent policy overrides global: browser is allowed again
|
||||||
expect(toolNames).not.toContain("browser");
|
expect(toolNames).toContain("browser");
|
||||||
expect(toolNames).not.toContain("bash");
|
expect(toolNames).not.toContain("bash");
|
||||||
expect(toolNames).not.toContain("process");
|
expect(toolNames).not.toContain("process");
|
||||||
});
|
});
|
||||||
@@ -213,4 +213,30 @@ describe("Agent-specific tool filtering", () => {
|
|||||||
expect(toolNames).not.toContain("bash");
|
expect(toolNames).not.toContain("bash");
|
||||||
expect(toolNames).not.toContain("write");
|
expect(toolNames).not.toContain("write");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should run bash synchronously when process is denied", async () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agent: {
|
||||||
|
tools: {
|
||||||
|
deny: ["process"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tools = createClawdbotCodingTools({
|
||||||
|
config: cfg,
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
workspaceDir: "/tmp/test-main",
|
||||||
|
agentDir: "/tmp/agent-main",
|
||||||
|
});
|
||||||
|
const bash = tools.find((tool) => tool.name === "bash");
|
||||||
|
expect(bash).toBeDefined();
|
||||||
|
|
||||||
|
const result = await bash?.execute("call1", {
|
||||||
|
command: "node -e \"setTimeout(() => { console.log('done') }, 50)\"",
|
||||||
|
yieldMs: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result?.details.status).toBe("completed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -432,6 +432,24 @@ function filterToolsByPolicy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isToolAllowedByPolicy(name: string, policy?: SandboxToolPolicy) {
|
||||||
|
if (!policy) return true;
|
||||||
|
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();
|
||||||
|
if (deny.has(normalized)) return false;
|
||||||
|
if (allow) return allow.has(normalized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToolAllowedByPolicies(
|
||||||
|
name: string,
|
||||||
|
policies: Array<SandboxToolPolicy | undefined>,
|
||||||
|
) {
|
||||||
|
return policies.every((policy) => isToolAllowedByPolicy(name, policy));
|
||||||
|
}
|
||||||
|
|
||||||
function wrapSandboxPathGuard(tool: AnyAgentTool, root: string): AnyAgentTool {
|
function wrapSandboxPathGuard(tool: AnyAgentTool, root: string): AnyAgentTool {
|
||||||
return {
|
return {
|
||||||
...tool,
|
...tool,
|
||||||
@@ -595,6 +613,25 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
}): AnyAgentTool[] {
|
}): AnyAgentTool[] {
|
||||||
const bashToolName = "bash";
|
const bashToolName = "bash";
|
||||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||||
|
const agentConfig =
|
||||||
|
options?.sessionKey && options?.config
|
||||||
|
? resolveAgentConfig(
|
||||||
|
options.config,
|
||||||
|
resolveAgentIdFromSessionKey(options.sessionKey),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
const hasAgentTools = agentConfig?.tools !== undefined;
|
||||||
|
const globalTools = options?.config?.agent?.tools;
|
||||||
|
const effectiveToolsPolicy = hasAgentTools ? agentConfig?.tools : globalTools;
|
||||||
|
const subagentPolicy =
|
||||||
|
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
||||||
|
? resolveSubagentToolPolicy(options.config)
|
||||||
|
: undefined;
|
||||||
|
const allowBackground = isToolAllowedByPolicies("process", [
|
||||||
|
effectiveToolsPolicy,
|
||||||
|
sandbox?.tools,
|
||||||
|
subagentPolicy,
|
||||||
|
]);
|
||||||
const sandboxRoot = sandbox?.workspaceDir;
|
const sandboxRoot = sandbox?.workspaceDir;
|
||||||
const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";
|
const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";
|
||||||
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
||||||
@@ -611,6 +648,7 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
});
|
});
|
||||||
const bashTool = createBashTool({
|
const bashTool = createBashTool({
|
||||||
...options?.bash,
|
...options?.bash,
|
||||||
|
allowBackground,
|
||||||
sandbox: sandbox
|
sandbox: sandbox
|
||||||
? {
|
? {
|
||||||
containerName: sandbox.containerName,
|
containerName: sandbox.containerName,
|
||||||
@@ -656,33 +694,15 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
if (tool.name === "whatsapp") return allowWhatsApp;
|
if (tool.name === "whatsapp") return allowWhatsApp;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
const globallyFiltered =
|
const toolsFiltered = effectiveToolsPolicy
|
||||||
options?.config?.agent?.tools &&
|
? filterToolsByPolicy(filtered, effectiveToolsPolicy)
|
||||||
(options.config.agent.tools.allow?.length ||
|
: filtered;
|
||||||
options.config.agent.tools.deny?.length)
|
|
||||||
? filterToolsByPolicy(filtered, options.config.agent.tools)
|
|
||||||
: filtered;
|
|
||||||
|
|
||||||
// Agent-specific tool policy
|
|
||||||
let agentFiltered = globallyFiltered;
|
|
||||||
if (options?.sessionKey && options?.config) {
|
|
||||||
const agentId = resolveAgentIdFromSessionKey(options.sessionKey);
|
|
||||||
const agentConfig = resolveAgentConfig(options.config, agentId);
|
|
||||||
if (agentConfig?.tools) {
|
|
||||||
agentFiltered = filterToolsByPolicy(globallyFiltered, agentConfig.tools);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sandboxed = sandbox
|
const sandboxed = sandbox
|
||||||
? filterToolsByPolicy(agentFiltered, sandbox.tools)
|
? filterToolsByPolicy(toolsFiltered, sandbox.tools)
|
||||||
: agentFiltered;
|
: toolsFiltered;
|
||||||
const subagentFiltered =
|
const subagentFiltered = subagentPolicy
|
||||||
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
? filterToolsByPolicy(sandboxed, subagentPolicy)
|
||||||
? filterToolsByPolicy(
|
: sandboxed;
|
||||||
sandboxed,
|
|
||||||
resolveSubagentToolPolicy(options.config),
|
|
||||||
)
|
|
||||||
: sandboxed;
|
|
||||||
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
|
// 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.
|
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
|
||||||
return subagentFiltered.map(normalizeToolParameters);
|
return subagentFiltered.map(normalizeToolParameters);
|
||||||
|
|||||||
Reference in New Issue
Block a user