mirror of https://github.com/openclaw/openclaw.git
feat(agents): add sessions_spawn sandbox require mode
This commit is contained in:
parent
a6a742f3d0
commit
bfeadb80b6
|
|
@ -156,6 +156,7 @@ Parameters:
|
|||
- `thread?` (default false; request thread-bound routing for this spawn when supported by the channel/plugin)
|
||||
- `mode?` (`run|session`; defaults to `run`, but defaults to `session` when `thread=true`; `mode="session"` requires `thread=true`)
|
||||
- `cleanup?` (`delete|keep`, default `keep`)
|
||||
- `sandbox?` (`inherit|require`, default `inherit`; `require` rejects spawn unless the target child runtime is sandboxed)
|
||||
|
||||
Allowlist:
|
||||
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ Core parameters:
|
|||
- `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none)
|
||||
- `sessions_history`: `sessionKey` (or `sessionId`), `limit?`, `includeTools?`
|
||||
- `sessions_send`: `sessionKey` (or `sessionId`), `message`, `timeoutSeconds?` (0 = fire-and-forget)
|
||||
- `sessions_spawn`: `task`, `label?`, `runtime?`, `agentId?`, `model?`, `thinking?`, `cwd?`, `runTimeoutSeconds?`, `thread?`, `mode?`, `cleanup?`
|
||||
- `sessions_spawn`: `task`, `label?`, `runtime?`, `agentId?`, `model?`, `thinking?`, `cwd?`, `runTimeoutSeconds?`, `thread?`, `mode?`, `cleanup?`, `sandbox?`
|
||||
- `session_status`: `sessionKey?` (default current; accepts `sessionId`), `model?` (`default` clears override)
|
||||
|
||||
Notes:
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ Tool params:
|
|||
- if `thread: true` and `mode` omitted, default becomes `session`
|
||||
- `mode: "session"` requires `thread: true`
|
||||
- `cleanup?` (`delete|keep`, default `keep`)
|
||||
- `sandbox?` (`inherit|require`, default `inherit`; `require` rejects spawn unless target child runtime is sandboxed)
|
||||
|
||||
## Thread-bound sessions
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ describe("sessions tools", () => {
|
|||
expect(schemaProp("sessions_spawn", "runTimeoutSeconds").type).toBe("number");
|
||||
expect(schemaProp("sessions_spawn", "thread").type).toBe("boolean");
|
||||
expect(schemaProp("sessions_spawn", "mode").type).toBe("string");
|
||||
expect(schemaProp("sessions_spawn", "sandbox").type).toBe("string");
|
||||
expect(schemaProp("sessions_spawn", "runtime").type).toBe("string");
|
||||
expect(schemaProp("sessions_spawn", "cwd").type).toBe("string");
|
||||
expect(schemaProp("subagents", "recentMinutes").type).toBe("number");
|
||||
|
|
|
|||
|
|
@ -47,12 +47,12 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => {
|
|||
return () => childSessionKey;
|
||||
}
|
||||
|
||||
async function executeSpawn(callId: string, agentId: string) {
|
||||
async function executeSpawn(callId: string, agentId: string, sandbox?: "inherit" | "require") {
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "whatsapp",
|
||||
});
|
||||
return tool.execute(callId, { task: "do thing", agentId });
|
||||
return tool.execute(callId, { task: "do thing", agentId, sandbox });
|
||||
}
|
||||
|
||||
async function expectAllowedSpawn(params: {
|
||||
|
|
@ -191,4 +191,36 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => {
|
|||
expect(details.error).toContain("Sandboxed sessions cannot spawn unsandboxed subagents.");
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forbids sandbox="require" when target runtime is unsandboxed', async () => {
|
||||
setSessionsSpawnConfigOverride({
|
||||
session: {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
subagents: {
|
||||
allowAgents: ["research"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "research",
|
||||
sandbox: {
|
||||
mode: "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeSpawn("call12", "research", "require");
|
||||
const details = result.details as { status?: string; error?: string };
|
||||
|
||||
expect(details.status).toBe("forbidden");
|
||||
expect(details.error).toContain('sandbox="require"');
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import {
|
|||
|
||||
export const SUBAGENT_SPAWN_MODES = ["run", "session"] as const;
|
||||
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];
|
||||
|
||||
export type SpawnSubagentParams = {
|
||||
task: string;
|
||||
|
|
@ -37,6 +39,7 @@ export type SpawnSubagentParams = {
|
|||
thread?: boolean;
|
||||
mode?: SpawnSubagentMode;
|
||||
cleanup?: "delete" | "keep";
|
||||
sandbox?: SpawnSubagentSandboxMode;
|
||||
expectsCompletionMessage?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -174,6 +177,7 @@ export async function spawnSubagentDirect(
|
|||
const modelOverride = params.model;
|
||||
const thinkingOverrideRaw = params.thinking;
|
||||
const requestThreadBinding = params.thread === true;
|
||||
const sandboxMode = params.sandbox === "require" ? "require" : "inherit";
|
||||
const spawnMode = resolveSpawnMode({
|
||||
requestedMode: params.mode,
|
||||
threadRequested: requestThreadBinding,
|
||||
|
|
@ -278,11 +282,18 @@ export async function spawnSubagentDirect(
|
|||
cfg,
|
||||
sessionKey: childSessionKey,
|
||||
});
|
||||
if (requesterRuntime.sandboxed && !childRuntime.sandboxed) {
|
||||
if (!childRuntime.sandboxed && (requesterRuntime.sandboxed || sandboxMode === "require")) {
|
||||
if (requesterRuntime.sandboxed) {
|
||||
return {
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Sandboxed sessions cannot spawn unsandboxed subagents. Set a sandboxed target agent or use the same agent runtime.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Sandboxed sessions cannot spawn unsandboxed subagents. Set a sandboxed target agent or use the same agent runtime.",
|
||||
'sessions_spawn sandbox="require" needs a sandboxed target runtime. Pick a sandboxed agentId or use sandbox="inherit".',
|
||||
};
|
||||
}
|
||||
const childDepth = callerDepth + 1;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ describe("sessions_spawn tool", () => {
|
|||
thread: true,
|
||||
mode: "session",
|
||||
cleanup: "keep",
|
||||
sandbox: "require",
|
||||
});
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
|
|
@ -70,6 +71,7 @@ describe("sessions_spawn tool", () => {
|
|||
thread: true,
|
||||
mode: "session",
|
||||
cleanup: "keep",
|
||||
sandbox: "require",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
agentSessionKey: "agent:main:main",
|
||||
|
|
@ -78,6 +80,25 @@ describe("sessions_spawn tool", () => {
|
|||
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('defaults sandbox to "inherit" for subagent runtime', async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentChannel: "discord",
|
||||
});
|
||||
|
||||
await tool.execute("call-sandbox-default", {
|
||||
task: "summarize logs",
|
||||
agentId: "main",
|
||||
});
|
||||
|
||||
expect(hoisted.spawnSubagentDirectMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sandbox: "inherit",
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes to ACP runtime when runtime=acp", async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { AnyAgentTool } from "./common.js";
|
|||
import { jsonResult, readStringParam } from "./common.js";
|
||||
|
||||
const SESSIONS_SPAWN_RUNTIMES = ["subagent", "acp"] as const;
|
||||
const SESSIONS_SPAWN_SANDBOX_MODES = ["inherit", "require"] as const;
|
||||
|
||||
const SessionsSpawnToolSchema = Type.Object({
|
||||
task: Type.String(),
|
||||
|
|
@ -22,6 +23,7 @@ const SessionsSpawnToolSchema = Type.Object({
|
|||
thread: Type.Optional(Type.Boolean()),
|
||||
mode: optionalStringEnum(SUBAGENT_SPAWN_MODES),
|
||||
cleanup: optionalStringEnum(["delete", "keep"] as const),
|
||||
sandbox: optionalStringEnum(SESSIONS_SPAWN_SANDBOX_MODES),
|
||||
});
|
||||
|
||||
export function createSessionsSpawnTool(opts?: {
|
||||
|
|
@ -55,6 +57,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||
const mode = params.mode === "run" || params.mode === "session" ? params.mode : undefined;
|
||||
const cleanup =
|
||||
params.cleanup === "keep" || params.cleanup === "delete" ? params.cleanup : "keep";
|
||||
const sandbox = params.sandbox === "require" ? "require" : "inherit";
|
||||
// Back-compat: older callers used timeoutSeconds for this tool.
|
||||
const timeoutSecondsCandidate =
|
||||
typeof params.runTimeoutSeconds === "number"
|
||||
|
|
@ -98,6 +101,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||
thread,
|
||||
mode,
|
||||
cleanup,
|
||||
sandbox,
|
||||
expectsCompletionMessage: true,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue