mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 12:54:58 +00:00
fix(agents): add strict format validation to sessions_spawn for agentId
Implements a strict format validation for the agentId parameter in sessions_spawn to fully resolve the ghost workspace creation bug reported in #31311. This fix introduces a regex format gate at the entry point to immediately reject malformed agentId strings. This prevents error messages (e.g., 'Agent not found: xyz') or path traversals from being mangled by normalizeAgentId into seemingly valid IDs (e.g., 'agent-not-found--xyz'), which was the root cause of the bug. The validation is placed before normalization and does not interfere with existing workflows, including delegating to agents that are allowlisted but not globally configured. New, non-redundant tests are added to sessions-spawn.allowlist.test.ts to cover format validation and ensure no regressions in allowlist behavior. Fixes #31311
This commit is contained in:
@@ -223,4 +223,124 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => {
|
||||
expect(details.error).toContain('sandbox="require"');
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// agentId format validation (#31311)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("rejects error-message-like strings as agentId (#31311)", async () => {
|
||||
setSessionsSpawnConfigOverride({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
list: [{ id: "main", subagents: { allowAgents: ["*"] } }, { id: "research" }],
|
||||
},
|
||||
});
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "whatsapp",
|
||||
});
|
||||
const result = await tool.execute("call-err-msg", {
|
||||
task: "do thing",
|
||||
agentId: "Agent not found: xyz",
|
||||
});
|
||||
const details = result.details as { status?: string; error?: string };
|
||||
expect(details.status).toBe("error");
|
||||
expect(details.error).toContain("Invalid agentId");
|
||||
expect(details.error).toContain("agents_list");
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects agentId containing path separators (#31311)", async () => {
|
||||
setSessionsSpawnConfigOverride({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
list: [{ id: "main", subagents: { allowAgents: ["*"] } }],
|
||||
},
|
||||
});
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "whatsapp",
|
||||
});
|
||||
const result = await tool.execute("call-path", {
|
||||
task: "do thing",
|
||||
agentId: "../../../etc/passwd",
|
||||
});
|
||||
const details = result.details as { status?: string; error?: string };
|
||||
expect(details.status).toBe("error");
|
||||
expect(details.error).toContain("Invalid agentId");
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects agentId exceeding 64 characters (#31311)", async () => {
|
||||
setSessionsSpawnConfigOverride({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
list: [{ id: "main", subagents: { allowAgents: ["*"] } }],
|
||||
},
|
||||
});
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "whatsapp",
|
||||
});
|
||||
const result = await tool.execute("call-long", {
|
||||
task: "do thing",
|
||||
agentId: "a".repeat(65),
|
||||
});
|
||||
const details = result.details as { status?: string; error?: string };
|
||||
expect(details.status).toBe("error");
|
||||
expect(details.error).toContain("Invalid agentId");
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts well-formed agentId with hyphens and underscores (#31311)", async () => {
|
||||
setSessionsSpawnConfigOverride({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
list: [{ id: "main", subagents: { allowAgents: ["*"] } }, { id: "my-research_agent01" }],
|
||||
},
|
||||
});
|
||||
callGatewayMock.mockImplementation(async () => ({
|
||||
runId: "run-1",
|
||||
status: "accepted",
|
||||
acceptedAt: 1000,
|
||||
}));
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "whatsapp",
|
||||
});
|
||||
const result = await tool.execute("call-valid", {
|
||||
task: "do thing",
|
||||
agentId: "my-research_agent01",
|
||||
});
|
||||
const details = result.details as { status?: string };
|
||||
expect(details.status).toBe("accepted");
|
||||
});
|
||||
|
||||
it("allows allowlisted-but-unconfigured agentId (#31311)", async () => {
|
||||
setSessionsSpawnConfigOverride({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", subagents: { allowAgents: ["research"] } },
|
||||
// "research" is NOT in agents.list — only in allowAgents
|
||||
],
|
||||
},
|
||||
});
|
||||
callGatewayMock.mockImplementation(async () => ({
|
||||
runId: "run-1",
|
||||
status: "accepted",
|
||||
acceptedAt: 1000,
|
||||
}));
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "whatsapp",
|
||||
});
|
||||
const result = await tool.execute("call-unconfigured", {
|
||||
task: "do thing",
|
||||
agentId: "research",
|
||||
});
|
||||
const details = result.details as { status?: string };
|
||||
// Must pass: "research" is in allowAgents even though not in agents.list
|
||||
expect(details.status).toBe("accepted");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user