openclaw/src/agents/openclaw-tools.subagents.sc...

246 lines
7.3 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it } from "vitest";
import {
callGatewayMock,
resetSubagentsConfigOverride,
setSubagentsConfigOverride,
} from "./openclaw-tools.subagents.test-harness.js";
import { addSubagentRunForTests, resetSubagentRegistryForTests } from "./subagent-registry.js";
import "./test-helpers/fast-core-tools.js";
import { createPerSenderSessionConfig } from "./test-helpers/session-config.js";
import { createSubagentsTool } from "./tools/subagents-tool.js";
function writeStore(storePath: string, store: Record<string, unknown>) {
fs.mkdirSync(path.dirname(storePath), { recursive: true });
fs.writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
}
describe("openclaw-tools: subagents scope isolation", () => {
let storePath = "";
beforeEach(() => {
resetSubagentRegistryForTests();
resetSubagentsConfigOverride();
callGatewayMock.mockReset();
storePath = path.join(
os.tmpdir(),
`openclaw-subagents-scope-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
);
setSubagentsConfigOverride({
session: createPerSenderSessionConfig({ store: storePath }),
});
writeStore(storePath, {});
});
it("leaf subagents do not inherit parent sibling control scope", async () => {
const leafKey = "agent:main:subagent:leaf";
const siblingKey = "agent:main:subagent:unsandboxed";
writeStore(storePath, {
[leafKey]: {
sessionId: "leaf-session",
updatedAt: Date.now(),
spawnedBy: "agent:main:main",
},
[siblingKey]: {
sessionId: "sibling-session",
updatedAt: Date.now(),
spawnedBy: "agent:main:main",
},
});
addSubagentRunForTests({
runId: "run-leaf",
childSessionKey: leafKey,
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
task: "sandboxed leaf",
cleanup: "keep",
createdAt: Date.now() - 30_000,
startedAt: Date.now() - 30_000,
});
addSubagentRunForTests({
runId: "run-sibling",
childSessionKey: siblingKey,
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
task: "unsandboxed sibling",
cleanup: "keep",
createdAt: Date.now() - 20_000,
startedAt: Date.now() - 20_000,
});
const tool = createSubagentsTool({ agentSessionKey: leafKey });
const result = await tool.execute("call-leaf-list", { action: "list" });
expect(result.details).toMatchObject({
status: "ok",
requesterSessionKey: leafKey,
callerSessionKey: leafKey,
callerIsSubagent: true,
total: 0,
active: [],
recent: [],
});
expect(callGatewayMock).not.toHaveBeenCalled();
});
it("orchestrator subagents still see children they spawned", async () => {
const orchestratorKey = "agent:main:subagent:orchestrator";
const workerKey = `${orchestratorKey}:subagent:worker`;
const siblingKey = "agent:main:subagent:sibling";
writeStore(storePath, {
[orchestratorKey]: {
sessionId: "orchestrator-session",
updatedAt: Date.now(),
spawnedBy: "agent:main:main",
},
[workerKey]: {
sessionId: "worker-session",
updatedAt: Date.now(),
spawnedBy: orchestratorKey,
},
[siblingKey]: {
sessionId: "sibling-session",
updatedAt: Date.now(),
spawnedBy: "agent:main:main",
},
});
addSubagentRunForTests({
runId: "run-worker",
childSessionKey: workerKey,
requesterSessionKey: orchestratorKey,
requesterDisplayKey: orchestratorKey,
task: "worker child",
cleanup: "keep",
createdAt: Date.now() - 30_000,
startedAt: Date.now() - 30_000,
});
addSubagentRunForTests({
runId: "run-sibling",
childSessionKey: siblingKey,
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
task: "sibling of orchestrator",
cleanup: "keep",
createdAt: Date.now() - 20_000,
startedAt: Date.now() - 20_000,
});
const tool = createSubagentsTool({ agentSessionKey: orchestratorKey });
const result = await tool.execute("call-orchestrator-list", { action: "list" });
const details = result.details as {
status?: string;
requesterSessionKey?: string;
total?: number;
active?: Array<{ sessionKey?: string }>;
};
expect(details.status).toBe("ok");
expect(details.requesterSessionKey).toBe(orchestratorKey);
expect(details.total).toBe(1);
expect(details.active).toEqual([
expect.objectContaining({
sessionKey: workerKey,
}),
]);
});
it("leaf subagents cannot kill even explicitly-owned child sessions", async () => {
const leafKey = "agent:main:subagent:leaf";
const childKey = `${leafKey}:subagent:child`;
writeStore(storePath, {
[leafKey]: {
sessionId: "leaf-session",
updatedAt: Date.now(),
spawnedBy: "agent:main:main",
subagentRole: "leaf",
subagentControlScope: "none",
},
[childKey]: {
sessionId: "child-session",
updatedAt: Date.now(),
spawnedBy: leafKey,
subagentRole: "leaf",
subagentControlScope: "none",
},
});
addSubagentRunForTests({
runId: "run-child",
childSessionKey: childKey,
controllerSessionKey: leafKey,
requesterSessionKey: leafKey,
requesterDisplayKey: leafKey,
task: "impossible child",
cleanup: "keep",
createdAt: Date.now() - 30_000,
startedAt: Date.now() - 30_000,
});
const tool = createSubagentsTool({ agentSessionKey: leafKey });
const result = await tool.execute("call-leaf-kill", {
action: "kill",
target: childKey,
});
expect(result.details).toMatchObject({
status: "forbidden",
error: "Leaf subagents cannot control other sessions.",
});
expect(callGatewayMock).not.toHaveBeenCalled();
});
it("leaf subagents cannot steer even explicitly-owned child sessions", async () => {
const leafKey = "agent:main:subagent:leaf";
const childKey = `${leafKey}:subagent:child`;
writeStore(storePath, {
[leafKey]: {
sessionId: "leaf-session",
updatedAt: Date.now(),
spawnedBy: "agent:main:main",
subagentRole: "leaf",
subagentControlScope: "none",
},
[childKey]: {
sessionId: "child-session",
updatedAt: Date.now(),
spawnedBy: leafKey,
subagentRole: "leaf",
subagentControlScope: "none",
},
});
addSubagentRunForTests({
runId: "run-child",
childSessionKey: childKey,
controllerSessionKey: leafKey,
requesterSessionKey: leafKey,
requesterDisplayKey: leafKey,
task: "impossible child",
cleanup: "keep",
createdAt: Date.now() - 30_000,
startedAt: Date.now() - 30_000,
});
const tool = createSubagentsTool({ agentSessionKey: leafKey });
const result = await tool.execute("call-leaf-steer", {
action: "steer",
target: childKey,
message: "continue",
});
expect(result.details).toMatchObject({
status: "forbidden",
error: "Leaf subagents cannot control other sessions.",
});
expect(callGatewayMock).not.toHaveBeenCalled();
});
});