fix(exec): apply per-agent exec defaults for opaque session keys

Co-authored-by: brin-tapcart <brin-tapcart@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-22 23:32:00 +01:00
parent 427b4360b9
commit 394a1af70f
9 changed files with 90 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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