fix: centralize per-agent exec defaults

This commit is contained in:
Peter Steinberger
2026-02-14 02:28:36 +01:00
parent 0a63548cb0
commit 9dfe5bdf23
5 changed files with 76 additions and 95 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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;

View File

@@ -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");
});
});

View File

@@ -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";