mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 03:56:45 +00:00
fix(acp): block sandboxed slash spawns
This commit is contained in:
@@ -158,6 +158,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/thread session lifecycle: reset thread-scoped sessions when a thread is archived so reopening a thread starts fresh without deleting transcript history. Thanks @thewilloftheshadow.
|
||||
- Discord/presence defaults: send an online presence update on ready when no custom presence is configured so bots no longer appear offline by default. Thanks @thewilloftheshadow.
|
||||
- Discord/typing cleanup: stop typing indicators after silent/NO_REPLY runs by marking the run complete before dispatch idle cleanup. Thanks @thewilloftheshadow.
|
||||
- ACP/sandbox spawn parity: block `/acp spawn` from sandboxed requester sessions with the same host-runtime guard already enforced for `sessions_spawn({ runtime: "acp" })`, preserving non-sandbox ACP flows while closing the command-path policy gap. Thanks @patte.
|
||||
- Discord/config SecretRef typing: align Discord account token config typing with SecretInput so SecretRef tokens typecheck. (#32490) Thanks @scoootscooob.
|
||||
- Discord/voice messages: request upload slots with JSON fetch calls so voice message uploads no longer fail with content-type errors. Thanks @thewilloftheshadow.
|
||||
- Discord/voice decoder fallback: drop the native Opus dependency and use opusscript for voice decoding to avoid native-opus installs. Thanks @thewilloftheshadow.
|
||||
|
||||
@@ -252,7 +252,7 @@ ACP sessions currently run on the host runtime, not inside the OpenClaw sandbox.
|
||||
|
||||
Current limitations:
|
||||
|
||||
- If the requester session is sandboxed, ACP spawns are blocked.
|
||||
- If the requester session is sandboxed, ACP spawns are blocked for both `sessions_spawn({ runtime: "acp" })` and `/acp spawn`.
|
||||
- Error: `Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.`
|
||||
- `sessions_spawn` with `runtime: "acp"` does not support `sandbox: "require"`.
|
||||
- Error: `sessions_spawn sandbox="require" is unsupported for runtime="acp" because ACP sessions run outside the sandbox. Use runtime="subagent" or sandbox="inherit".`
|
||||
|
||||
@@ -81,6 +81,27 @@ export const ACP_SPAWN_ACCEPTED_NOTE =
|
||||
export const ACP_SPAWN_SESSION_ACCEPTED_NOTE =
|
||||
"thread-bound ACP session stays active after this task; continue in-thread for follow-ups.";
|
||||
|
||||
export function resolveAcpSpawnRuntimePolicyError(params: {
|
||||
cfg: OpenClawConfig;
|
||||
requesterSessionKey?: string;
|
||||
requesterSandboxed?: boolean;
|
||||
sandbox?: SpawnAcpSandboxMode;
|
||||
}): string | undefined {
|
||||
const sandboxMode = params.sandbox === "require" ? "require" : "inherit";
|
||||
const requesterRuntime = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.requesterSessionKey,
|
||||
});
|
||||
const requesterSandboxed = params.requesterSandboxed === true || requesterRuntime.sandboxed;
|
||||
if (requesterSandboxed) {
|
||||
return 'Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.';
|
||||
}
|
||||
if (sandboxMode === "require") {
|
||||
return 'sessions_spawn sandbox="require" is unsupported for runtime="acp" because ACP sessions run outside the sandbox. Use runtime="subagent" or sandbox="inherit".';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type PreparedAcpThreadBinding = {
|
||||
channel: string;
|
||||
accountId: string;
|
||||
@@ -242,7 +263,6 @@ export async function spawnAcpDirect(
|
||||
error: "ACP is disabled by policy (`acp.enabled=false`).",
|
||||
};
|
||||
}
|
||||
const sandboxMode = params.sandbox === "require" ? "require" : "inherit";
|
||||
const streamToParentRequested = params.streamTo === "parent";
|
||||
const parentSessionKey = ctx.agentSessionKey?.trim();
|
||||
if (streamToParentRequested && !parentSessionKey) {
|
||||
@@ -251,23 +271,16 @@ export async function spawnAcpDirect(
|
||||
error: 'sessions_spawn streamTo="parent" requires an active requester session context.',
|
||||
};
|
||||
}
|
||||
const requesterRuntime = resolveSandboxRuntimeStatus({
|
||||
const runtimePolicyError = resolveAcpSpawnRuntimePolicyError({
|
||||
cfg,
|
||||
sessionKey: ctx.agentSessionKey,
|
||||
requesterSessionKey: ctx.agentSessionKey,
|
||||
requesterSandboxed: ctx.sandboxed,
|
||||
sandbox: params.sandbox,
|
||||
});
|
||||
const requesterSandboxed = ctx.sandboxed === true || requesterRuntime.sandboxed;
|
||||
if (requesterSandboxed) {
|
||||
if (runtimePolicyError) {
|
||||
return {
|
||||
status: "forbidden",
|
||||
error:
|
||||
'Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.',
|
||||
};
|
||||
}
|
||||
if (sandboxMode === "require") {
|
||||
return {
|
||||
status: "forbidden",
|
||||
error:
|
||||
'sessions_spawn sandbox="require" is unsupported for runtime="acp" because ACP sessions run outside the sandbox. Use runtime="subagent" or sandbox="inherit".',
|
||||
error: runtimePolicyError,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -592,6 +592,25 @@ describe("/acp command", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forbids /acp spawn from sandboxed requester sessions", async () => {
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: { mode: "all" },
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const result = await runDiscordAcpCommand("/acp spawn codex", cfg);
|
||||
|
||||
expect(result?.reply?.text).toContain("Sandboxed sessions cannot spawn ACP sessions");
|
||||
expect(hoisted.requireAcpRuntimeBackendMock).not.toHaveBeenCalled();
|
||||
expect(hoisted.ensureSessionMock).not.toHaveBeenCalled();
|
||||
expect(hoisted.sessionBindingBindMock).not.toHaveBeenCalled();
|
||||
expect(hoisted.callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels the ACP session bound to the current thread", async () => {
|
||||
mockBoundThreadSession({ state: "running" });
|
||||
const result = await runThreadAcpCommand("/acp cancel", baseCfg);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resolveAcpSessionCwd,
|
||||
resolveAcpThreadSessionDetailLines,
|
||||
} from "../../../acp/runtime/session-identifiers.js";
|
||||
import { resolveAcpSpawnRuntimePolicyError } from "../../../agents/acp-spawn.js";
|
||||
import {
|
||||
resolveThreadBindingIntroText,
|
||||
resolveThreadBindingThreadName,
|
||||
@@ -253,6 +254,13 @@ export async function handleAcpSpawnAction(
|
||||
}
|
||||
|
||||
const spawn = parsed.value;
|
||||
const runtimePolicyError = resolveAcpSpawnRuntimePolicyError({
|
||||
cfg: params.cfg,
|
||||
requesterSessionKey: params.sessionKey,
|
||||
});
|
||||
if (runtimePolicyError) {
|
||||
return stopWithText(`⚠️ ${runtimePolicyError}`);
|
||||
}
|
||||
const agentPolicyError = resolveAcpAgentPolicyError(params.cfg, spawn.agentId);
|
||||
if (agentPolicyError) {
|
||||
return stopWithText(
|
||||
|
||||
Reference in New Issue
Block a user