From 394a1af70f1d3d10542433948ff72dd71ff8a697 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 23:32:00 +0100 Subject: [PATCH] fix(exec): apply per-agent exec defaults for opaque session keys Co-authored-by: brin-tapcart --- CHANGELOG.md | 1 + src/agents/agent-scope.ts | 12 +++++- src/agents/cli-runner.ts | 1 + ...dded-runner.resolvesessionagentids.test.ts | 17 ++++++++ src/agents/pi-embedded-runner/run/attempt.ts | 24 ++++------- src/agents/pi-tools-agent-config.test.ts | 40 +++++++++++++++++++ src/agents/pi-tools.policy.ts | 10 ++++- src/agents/pi-tools.ts | 2 + .../reply/commands-system-prompt.ts | 2 + 9 files changed, 90 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eaf10faf..7cb88e168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Security/Voice Call: harden media stream WebSocket handling against pre-auth idle-connection DoS by adding strict pre-start timeouts, pending/per-IP connection limits, and total connection caps for streaming endpoints. This ships in the next npm release. Thanks @jiseoung for reporting. +- Agents/Exec: honor explicit agent context when resolving `tools.exec` defaults for runs with opaque/non-agent session keys, so per-agent `host/security/ask` policies are applied consistently. (#11832) - Telegram/Discord extensions: propagate trusted `mediaLocalRoots` through extension outbound `sendMedia` options so extension direct-send media paths honor agent-scoped local-media allowlists. (#20029, #21903, #23227) - Exec/Background: stop applying the default exec timeout to background sessions (`background: true` or explicit `yieldMs`) when no explicit timeout is set, so long-running background jobs are no longer terminated at the default timeout boundary. (#23303) - Plugins/Media sandbox: propagate trusted `mediaLocalRoots` through plugin action dispatch (including Discord/Telegram action adapters) so plugin send paths enforce the same agent-scoped local-media sandbox roots as core outbound sends. (#20258, #22718) diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index 53cd5c085..c1e5774e2 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -74,15 +74,23 @@ export function resolveDefaultAgentId(cfg: OpenClawConfig): string { return normalizeAgentId(chosen || DEFAULT_AGENT_ID); } -export function resolveSessionAgentIds(params: { sessionKey?: string; config?: OpenClawConfig }): { +export function resolveSessionAgentIds(params: { + sessionKey?: string; + config?: OpenClawConfig; + agentId?: string; +}): { defaultAgentId: string; sessionAgentId: string; } { const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); + const explicitAgentIdRaw = + typeof params.agentId === "string" ? params.agentId.trim().toLowerCase() : ""; + const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null; const sessionKey = params.sessionKey?.trim(); const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : undefined; const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null; - const sessionAgentId = parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId; + const sessionAgentId = + explicitAgentId ?? (parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId); return { defaultAgentId, sessionAgentId }; } diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index e8a7874b8..cc19546b5 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -96,6 +96,7 @@ export async function runCliAgent(params: { const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ sessionKey: params.sessionKey, config: params.config, + agentId: params.agentId, }); const heartbeatPrompt = sessionAgentId === defaultAgentId diff --git a/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts b/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts index 931ec2809..1bbecd4ce 100644 --- a/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts +++ b/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts @@ -48,4 +48,21 @@ describe("resolveSessionAgentIds", () => { }); expect(sessionAgentId).toBe("main"); }); + + it("uses explicit agentId when sessionKey is missing", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + agentId: "main", + config: cfg, + }); + expect(sessionAgentId).toBe("main"); + }); + + it("prefers explicit agentId over non-agent session keys", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + sessionKey: "telegram:slash:123", + agentId: "main", + config: cfg, + }); + expect(sessionAgentId).toBe("main"); + }); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index d0892148b..ae364723a 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -19,11 +19,7 @@ import type { PluginHookBeforeAgentStartResult, PluginHookBeforePromptBuildResult, } from "../../../plugins/types.js"; -import { - isCronSessionKey, - isSubagentSessionKey, - normalizeAgentId, -} from "../../../routing/session-key.js"; +import { isCronSessionKey, isSubagentSessionKey } from "../../../routing/session-key.js"; import { resolveSignalReactionLevel } from "../../../signal/reaction-level.js"; import { resolveTelegramInlineButtonsScope } from "../../../telegram/inline-buttons.js"; import { resolveTelegramReactionLevel } from "../../../telegram/reaction-level.js"; @@ -356,11 +352,17 @@ export async function runEmbeddedAttempt( const agentDir = params.agentDir ?? resolveOpenClawAgentDir(); + const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ + sessionKey: params.sessionKey, + config: params.config, + agentId: params.agentId, + }); // Check if the model supports native image input const modelHasVision = params.model.input?.includes("image") ?? false; const toolsRaw = params.disableTools ? [] : createOpenClawCodingTools({ + agentId: sessionAgentId, exec: { ...params.execOverrides, elevated: params.bashElevated, @@ -451,10 +453,6 @@ export async function runEmbeddedAttempt( return undefined; })() : undefined; - const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ - sessionKey: params.sessionKey, - config: params.config, - }); const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated); const reasoningTagHint = isReasoningTagProvider(params.provider); // Resolve channel-specific message actions for system prompt @@ -1009,13 +1007,7 @@ export async function runEmbeddedAttempt( } // Hook runner was already obtained earlier before tool creation - const hookAgentId = - typeof params.agentId === "string" && params.agentId.trim() - ? normalizeAgentId(params.agentId) - : resolveSessionAgentIds({ - sessionKey: params.sessionKey, - config: params.config, - }).sessionAgentId; + const hookAgentId = sessionAgentId; let promptError: unknown = null; let promptErrorSource: "prompt" | "compaction" | null = null; diff --git a/src/agents/pi-tools-agent-config.test.ts b/src/agents/pi-tools-agent-config.test.ts index 0e22e341f..a87fdf884 100644 --- a/src/agents/pi-tools-agent-config.test.ts +++ b/src/agents/pi-tools-agent-config.test.ts @@ -690,4 +690,44 @@ describe("Agent-specific tool filtering", () => { }), ).rejects.toThrow("exec host=sandbox is configured"); }); + + it("applies explicit agentId exec defaults when sessionKey is opaque", async () => { + const cfg: OpenClawConfig = { + tools: { + exec: { + host: "sandbox", + security: "full", + ask: "off", + }, + }, + agents: { + list: [ + { + id: "main", + tools: { + exec: { + host: "gateway", + }, + }, + }, + ], + }, + }; + + const tools = createOpenClawCodingTools({ + config: cfg, + agentId: "main", + sessionKey: "run-opaque-123", + workspaceDir: "/tmp/test-main-opaque-session", + agentDir: "/tmp/agent-main-opaque-session", + }); + const execTool = tools.find((tool) => tool.name === "exec"); + expect(execTool).toBeDefined(); + const result = await execTool!.execute("call-main-opaque-session", { + command: "echo done", + yieldMs: 1000, + }); + const details = result?.details as { status?: string } | undefined; + expect(details?.status).toBe("completed"); + }); }); diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index 9564d1554..db9a36755 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -2,6 +2,7 @@ import { getChannelDock } from "../channels/dock.js"; import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js"; +import { normalizeAgentId } from "../routing/session-key.js"; import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js"; @@ -198,10 +199,17 @@ function resolveProviderToolPolicy(params: { export function resolveEffectiveToolPolicy(params: { config?: OpenClawConfig; sessionKey?: string; + agentId?: string; modelProvider?: string; modelId?: string; }) { - const agentId = params.sessionKey ? resolveAgentIdFromSessionKey(params.sessionKey) : undefined; + const explicitAgentId = + typeof params.agentId === "string" && params.agentId.trim() + ? normalizeAgentId(params.agentId) + : undefined; + const agentId = + explicitAgentId ?? + (params.sessionKey ? resolveAgentIdFromSessionKey(params.sessionKey) : undefined); const agentConfig = params.config && agentId ? resolveAgentConfig(params.config, agentId) : undefined; const agentTools = agentConfig?.tools; diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 4edc9d423..361ca903f 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -169,6 +169,7 @@ export const __testing = { } as const; export function createOpenClawCodingTools(options?: { + agentId?: string; exec?: ExecToolDefaults & ProcessToolDefaults; messageProvider?: string; agentAccountId?: string; @@ -238,6 +239,7 @@ export function createOpenClawCodingTools(options?: { } = resolveEffectiveToolPolicy({ config: options?.config, sessionKey: options?.sessionKey, + agentId: options?.agentId, modelProvider: options?.modelProvider, modelId: options?.modelId, }); diff --git a/src/auto-reply/reply/commands-system-prompt.ts b/src/auto-reply/reply/commands-system-prompt.ts index abbedd689..f13c23690 100644 --- a/src/auto-reply/reply/commands-system-prompt.ts +++ b/src/auto-reply/reply/commands-system-prompt.ts @@ -54,6 +54,7 @@ export async function resolveCommandsSystemPromptBundle( try { return createOpenClawCodingTools({ config: params.cfg, + agentId: params.agentId, workspaceDir, sessionKey: params.sessionKey, messageProvider: params.command.channel, @@ -74,6 +75,7 @@ export async function resolveCommandsSystemPromptBundle( const { sessionAgentId } = resolveSessionAgentIds({ sessionKey: params.sessionKey, config: params.cfg, + agentId: params.agentId, }); const defaultModelRef = resolveDefaultModelForAgent({ cfg: params.cfg,