diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fca632b72..a9aaa93a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/Compaction: centralize exec default resolution in the shared tool factory so per-agent `tools.exec` overrides (host/security/ask/node and related defaults) persist across compaction retries. (#15833) Thanks @napetrov. - CLI/Completion: route plugin-load logs to stderr and write generated completion scripts directly to stdout to avoid `source <(openclaw completion ...)` corruption. (#15481) Thanks @arosstale. - Gateway/Agents: stop injecting a phantom `main` agent into gateway agent listings when `agents.list` explicitly excludes it. (#11450) Thanks @arosstale. - Agents/Heartbeat: stop auto-creating `HEARTBEAT.md` during workspace bootstrap so missing files continue to run heartbeat as documented. (#11766) Thanks @shadril238. diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index e31b6cdaeb..1ada5c626a 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -75,7 +75,7 @@ import { createSystemPromptOverride, } from "./system-prompt.js"; import { splitSdkTools } from "./tool-split.js"; -import { describeUnknownError, mapThinkingLevel, resolveAgentExecToolDefaults } from "./utils.js"; +import { describeUnknownError, mapThinkingLevel } from "./utils.js"; import { flushPendingToolResultsAfterIdle } from "./wait-for-idle-before-flush.js"; export type CompactEmbeddedPiSessionParams = { @@ -364,7 +364,6 @@ export async function compactEmbeddedPiSessionDirect( const runAbortController = new AbortController(); const toolsRaw = createOpenClawCodingTools({ exec: { - ...resolveAgentExecToolDefaults(params.config, params.sessionKey), elevated: params.bashElevated, }, sandbox, diff --git a/src/agents/pi-embedded-runner/utils.ts b/src/agents/pi-embedded-runner/utils.ts index 18e784c070..07fba6458c 100644 --- a/src/agents/pi-embedded-runner/utils.ts +++ b/src/agents/pi-embedded-runner/utils.ts @@ -1,9 +1,5 @@ import type { ThinkingLevel } from "@mariozechner/pi-agent-core"; import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js"; -import type { OpenClawConfig } from "../../config/config.js"; -import type { ExecToolDefaults } from "../bash-tools.js"; -import { logVerbose, danger } from "../../globals.js"; -import { resolveAgentConfig, resolveSessionAgentIds } from "../agent-scope.js"; export function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel { // pi-agent-core supports "xhigh"; OpenClaw enables it for specific models. @@ -13,81 +9,6 @@ export function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel { return level; } -export function resolveExecToolDefaults(config?: OpenClawConfig): ExecToolDefaults | undefined { - const tools = config?.tools; - if (!tools?.exec) { - return undefined; - } - return tools.exec; -} - -/** - * Resolve exec tool defaults with per-agent overrides merged over global defaults. - * - * This function is used during session compaction to preserve per-agent exec policy - * settings (security mode, approval requirements, etc.) that would otherwise be lost. - * - * @param config - OpenClaw configuration object - * @param sessionKey - Session key to derive agent ID (format: "agent:name:sessionId") - * @returns Merged exec defaults with per-agent overrides, or empty object if no config - */ -export function resolveAgentExecToolDefaults( - config?: OpenClawConfig, - sessionKey?: string, -): ExecToolDefaults { - if (!config) { - return {}; - } - - const globalExec = config.tools?.exec; - - if (!sessionKey || typeof sessionKey !== "string" || sessionKey.trim().length === 0) { - return globalExec ?? {}; - } - - try { - const resolved = resolveSessionAgentIds({ sessionKey, config }); - - if (!resolved || !resolved.sessionAgentId) { - return globalExec ?? {}; - } - - const { sessionAgentId } = resolved; - const agentConfig = resolveAgentConfig(config, sessionAgentId); - const agentExec = agentConfig?.tools?.exec; - - const merged: ExecToolDefaults = { - 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, - 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, - elevated: agentExec?.elevated ?? globalExec?.elevated, - allowBackground: agentExec?.allowBackground ?? globalExec?.allowBackground, - sandbox: agentExec?.sandbox ?? globalExec?.sandbox, - }; - - return merged; - } catch (err) { - const isExpectedError = err instanceof TypeError; - - if (isExpectedError) { - logVerbose(`Agent config resolution failed, using global defaults`); - return globalExec ?? {}; - } - - danger(`UNEXPECTED ERROR in resolveAgentExecToolDefaults:`, err); - throw err; - } -} - export function describeUnknownError(error: unknown): string { if (error instanceof Error) { return error.message; diff --git a/src/agents/pi-tools-agent-config.e2e.test.ts b/src/agents/pi-tools-agent-config.e2e.test.ts index 012c7e30c3..69e05c6077 100644 --- a/src/agents/pi-tools-agent-config.e2e.test.ts +++ b/src/agents/pi-tools-agent-config.e2e.test.ts @@ -535,4 +535,59 @@ describe("Agent-specific tool filtering", () => { expect(result?.details.status).toBe("completed"); }); + + it("should apply agent-specific exec host defaults over global defaults", async () => { + const cfg: OpenClawConfig = { + tools: { + exec: { + host: "sandbox", + }, + }, + agents: { + list: [ + { + id: "main", + tools: { + exec: { + host: "gateway", + }, + }, + }, + { + id: "helper", + }, + ], + }, + }; + + const mainTools = createOpenClawCodingTools({ + config: cfg, + sessionKey: "agent:main:main", + workspaceDir: "/tmp/test-main-exec-defaults", + agentDir: "/tmp/agent-main-exec-defaults", + }); + const mainExecTool = mainTools.find((tool) => tool.name === "exec"); + expect(mainExecTool).toBeDefined(); + await expect( + mainExecTool!.execute("call-main", { + command: "echo done", + host: "sandbox", + }), + ).rejects.toThrow("exec host not allowed"); + + const helperTools = createOpenClawCodingTools({ + config: cfg, + sessionKey: "agent:helper:main", + workspaceDir: "/tmp/test-helper-exec-defaults", + agentDir: "/tmp/agent-helper-exec-defaults", + }); + const helperExecTool = helperTools.find((tool) => tool.name === "exec"); + expect(helperExecTool).toBeDefined(); + const helperResult = await helperExecTool!.execute("call-helper", { + command: "echo done", + host: "sandbox", + yieldMs: 10, + }); + expect(helperResult?.details.status).toBe("completed"); + }); }); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index d3118fbbcc..26e16008c0 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -13,6 +13,7 @@ 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, @@ -86,21 +87,25 @@ function isApplyPatchAllowedForModel(params: { }); } -function resolveExecConfig(cfg: OpenClawConfig | undefined) { +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: globalExec?.host, - security: globalExec?.security, - ask: globalExec?.ask, - node: globalExec?.node, - pathPrepend: globalExec?.pathPrepend, - safeBins: globalExec?.safeBins, - backgroundMs: globalExec?.backgroundMs, - timeoutSec: globalExec?.timeoutSec, - approvalRunningNoticeMs: globalExec?.approvalRunningNoticeMs, - cleanupMs: globalExec?.cleanupMs, - notifyOnExit: globalExec?.notifyOnExit, - applyPatch: globalExec?.applyPatch, + 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, + 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, + applyPatch: agentExec?.applyPatch ?? globalExec?.applyPatch, }; } @@ -231,7 +236,7 @@ export function createOpenClawCodingTools(options?: { sandbox?.tools, subagentPolicy, ]); - const execConfig = resolveExecConfig(options?.config); + const execConfig = resolveExecConfig({ cfg: options?.config, agentId }); const sandboxRoot = sandbox?.workspaceDir; const sandboxFsBridge = sandbox?.fsBridge; const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";