From b28e472fa521b4c4907c72153501409439d3648f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 15:57:17 +0000 Subject: [PATCH] fix(agents): validate sessions_spawn agentId format (#31381) --- ...ols.subagents.sessions-spawn.allowlist.test.ts | 1 - src/agents/subagent-spawn.ts | 14 ++------------ src/routing/session-key.test.ts | 15 +++++++++++++++ src/routing/session-key.ts | 5 +++++ 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts index e46dab37baf..d66046268bc 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts @@ -223,7 +223,6 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => { expect(details.error).toContain('sandbox="require"'); expect(callGatewayMock).not.toHaveBeenCalled(); }); - // --------------------------------------------------------------------------- // agentId format validation (#31311) // --------------------------------------------------------------------------- diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 5a7b5b162bc..875d7e5526b 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -7,6 +7,7 @@ import { loadConfig } from "../config/config.js"; import { callGateway } from "../gateway/call.js"; import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; import { + isValidAgentId, isCronSessionKey, normalizeAgentId, parseAgentSessionKey, @@ -31,16 +32,6 @@ export type SpawnSubagentMode = (typeof SUBAGENT_SPAWN_MODES)[number]; export const SUBAGENT_SPAWN_SANDBOX_MODES = ["inherit", "require"] as const; export type SpawnSubagentSandboxMode = (typeof SUBAGENT_SPAWN_SANDBOX_MODES)[number]; -/** - * Strict format gate for user-supplied agentId values. - * Rejects error-message-like strings, path traversals, and other - * values that would be silently mangled by {@link normalizeAgentId} - * and could create ghost workspace directories. - * - * Must stay in sync with the canonical `VALID_ID_RE` in session-key.ts. - */ -const STRICT_AGENT_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i; - export function decodeStrictBase64(value: string, maxDecodedBytes: number): Buffer | null { const maxEncodedBytes = Math.ceil(maxDecodedBytes / 3) * 4; if (value.length > maxEncodedBytes * 2) { @@ -264,13 +255,12 @@ export async function spawnSubagentDirect( // Without this gate, error-message strings like "Agent not found: xyz" pass // through normalizeAgentId and become "agent-not-found--xyz", which later // creates ghost workspace directories and triggers cascading cron loops (#31311). - if (requestedAgentId && !STRICT_AGENT_ID_RE.test(requestedAgentId)) { + if (requestedAgentId && !isValidAgentId(requestedAgentId)) { return { status: "error", error: `Invalid agentId "${requestedAgentId}". Agent IDs must match [a-z0-9][a-z0-9_-]{0,63}. Use agents_list to discover valid targets.`, }; } - const modelOverride = params.model; const thinkingOverrideRaw = params.thinking; const requestThreadBinding = params.thread === true; diff --git a/src/routing/session-key.test.ts b/src/routing/session-key.test.ts index 044b7b8a743..777871ca412 100644 --- a/src/routing/session-key.test.ts +++ b/src/routing/session-key.test.ts @@ -6,6 +6,7 @@ import { } from "../sessions/session-key-utils.js"; import { classifySessionKeyShape, + isValidAgentId, parseAgentSessionKey, toAgentStoreSessionKey, } from "./session-key.js"; @@ -115,3 +116,17 @@ describe("session key canonicalization", () => { ).toBe("agent:main:main"); }); }); + +describe("isValidAgentId", () => { + it("accepts valid agent ids", () => { + expect(isValidAgentId("main")).toBe(true); + expect(isValidAgentId("my-research_agent01")).toBe(true); + }); + + it("rejects malformed agent ids", () => { + expect(isValidAgentId("")).toBe(false); + expect(isValidAgentId("Agent not found: xyz")).toBe(false); + expect(isValidAgentId("../../../etc/passwd")).toBe(false); + expect(isValidAgentId("a".repeat(65))).toBe(false); + }); +}); diff --git a/src/routing/session-key.ts b/src/routing/session-key.ts index 50481e4bded..88e42dad3fa 100644 --- a/src/routing/session-key.ts +++ b/src/routing/session-key.ts @@ -99,6 +99,11 @@ export function normalizeAgentId(value: string | undefined | null): string { ); } +export function isValidAgentId(value: string | undefined | null): boolean { + const trimmed = (value ?? "").trim(); + return Boolean(trimmed) && VALID_ID_RE.test(trimmed); +} + export function sanitizeAgentId(value: string | undefined | null): string { return normalizeAgentId(value); }