mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix: centralize per-agent exec defaults
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user