mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:48:38 +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/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/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.
|
- 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/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 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.
|
- 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:
|
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.`
|
- 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"`.
|
- `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".`
|
- 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 =
|
export const ACP_SPAWN_SESSION_ACCEPTED_NOTE =
|
||||||
"thread-bound ACP session stays active after this task; continue in-thread for follow-ups.";
|
"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 = {
|
type PreparedAcpThreadBinding = {
|
||||||
channel: string;
|
channel: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
@@ -242,7 +263,6 @@ export async function spawnAcpDirect(
|
|||||||
error: "ACP is disabled by policy (`acp.enabled=false`).",
|
error: "ACP is disabled by policy (`acp.enabled=false`).",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const sandboxMode = params.sandbox === "require" ? "require" : "inherit";
|
|
||||||
const streamToParentRequested = params.streamTo === "parent";
|
const streamToParentRequested = params.streamTo === "parent";
|
||||||
const parentSessionKey = ctx.agentSessionKey?.trim();
|
const parentSessionKey = ctx.agentSessionKey?.trim();
|
||||||
if (streamToParentRequested && !parentSessionKey) {
|
if (streamToParentRequested && !parentSessionKey) {
|
||||||
@@ -251,23 +271,16 @@ export async function spawnAcpDirect(
|
|||||||
error: 'sessions_spawn streamTo="parent" requires an active requester session context.',
|
error: 'sessions_spawn streamTo="parent" requires an active requester session context.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const requesterRuntime = resolveSandboxRuntimeStatus({
|
const runtimePolicyError = resolveAcpSpawnRuntimePolicyError({
|
||||||
cfg,
|
cfg,
|
||||||
sessionKey: ctx.agentSessionKey,
|
requesterSessionKey: ctx.agentSessionKey,
|
||||||
|
requesterSandboxed: ctx.sandboxed,
|
||||||
|
sandbox: params.sandbox,
|
||||||
});
|
});
|
||||||
const requesterSandboxed = ctx.sandboxed === true || requesterRuntime.sandboxed;
|
if (runtimePolicyError) {
|
||||||
if (requesterSandboxed) {
|
|
||||||
return {
|
return {
|
||||||
status: "forbidden",
|
status: "forbidden",
|
||||||
error:
|
error: runtimePolicyError,
|
||||||
'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".',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
it("cancels the ACP session bound to the current thread", async () => {
|
||||||
mockBoundThreadSession({ state: "running" });
|
mockBoundThreadSession({ state: "running" });
|
||||||
const result = await runThreadAcpCommand("/acp cancel", baseCfg);
|
const result = await runThreadAcpCommand("/acp cancel", baseCfg);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
resolveAcpSessionCwd,
|
resolveAcpSessionCwd,
|
||||||
resolveAcpThreadSessionDetailLines,
|
resolveAcpThreadSessionDetailLines,
|
||||||
} from "../../../acp/runtime/session-identifiers.js";
|
} from "../../../acp/runtime/session-identifiers.js";
|
||||||
|
import { resolveAcpSpawnRuntimePolicyError } from "../../../agents/acp-spawn.js";
|
||||||
import {
|
import {
|
||||||
resolveThreadBindingIntroText,
|
resolveThreadBindingIntroText,
|
||||||
resolveThreadBindingThreadName,
|
resolveThreadBindingThreadName,
|
||||||
@@ -253,6 +254,13 @@ export async function handleAcpSpawnAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const spawn = parsed.value;
|
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);
|
const agentPolicyError = resolveAcpAgentPolicyError(params.cfg, spawn.agentId);
|
||||||
if (agentPolicyError) {
|
if (agentPolicyError) {
|
||||||
return stopWithText(
|
return stopWithText(
|
||||||
|
|||||||
Reference in New Issue
Block a user