mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 06:47:26 +00:00
fix(security): enforce sandbox inheritance for sessions_spawn
This commit is contained in:
@@ -97,6 +97,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- Security/Sandbox media reads: eliminate sandbox media TOCTOU symlink-retarget escapes by enforcing root-scoped boundary-safe reads at attachment/image load time and consolidating shared safe-read helpers across sandbox media callsites. This ships in the next npm release. Thanks @tdjackey for reporting.
|
- Security/Sandbox media reads: eliminate sandbox media TOCTOU symlink-retarget escapes by enforcing root-scoped boundary-safe reads at attachment/image load time and consolidating shared safe-read helpers across sandbox media callsites. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||||
|
- Security/Subagents sandbox inheritance: block sandboxed sessions from spawning cross-agent subagents that would run unsandboxed, preventing runtime sandbox downgrade via `sessions_spawn agentId`. Thanks @tdjackey for reporting.
|
||||||
- Security/Node metadata policy: harden node platform classification against Unicode confusables and switch unknown platform defaults to a conservative allowlist that excludes `system.run`/`system.which` unless explicitly allowlisted, preventing metadata canonicalization drift from broadening node command permissions. Thanks @tdjackey for reporting.
|
- Security/Node metadata policy: harden node platform classification against Unicode confusables and switch unknown platform defaults to a conservative allowlist that excludes `system.run`/`system.which` unless explicitly allowlisted, preventing metadata canonicalization drift from broadening node command permissions. Thanks @tdjackey for reporting.
|
||||||
- Plugins/Discovery precedence: load bundled plugins before auto-discovered global extensions so bundled channel plugins win duplicate-ID resolution by default (explicit `plugins.load.paths` overrides remain highest precedence), with loader regression coverage. Landed from contributor PR #29710 by @Sid-Qin. Thanks @Sid-Qin.
|
- Plugins/Discovery precedence: load bundled plugins before auto-discovered global extensions so bundled channel plugins win duplicate-ID resolution by default (explicit `plugins.load.paths` overrides remain highest precedence), with loader regression coverage. Landed from contributor PR #29710 by @Sid-Qin. Thanks @Sid-Qin.
|
||||||
- Discord/Reconnect integrity: release Discord message listener lane immediately while preserving serialized handler execution, add HELLO-stall resume-first recovery with bounded fresh-identify fallback after repeated stalls, and extend lifecycle/listener regression coverage for forced reconnect scenarios. Landed from contributor PR #29508 by @cgdusek. Thanks @cgdusek.
|
- Discord/Reconnect integrity: release Discord message listener lane immediately while preserving serialized handler execution, add HELLO-stall resume-first recovery with bounded fresh-identify fallback after repeated stalls, and extend lifecycle/listener regression coverage for forced reconnect scenarios. Landed from contributor PR #29508 by @cgdusek. Thanks @cgdusek.
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ Parameters:
|
|||||||
Allowlist:
|
Allowlist:
|
||||||
|
|
||||||
- `agents.list[].subagents.allowAgents`: list of agent ids allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
- `agents.list[].subagents.allowAgents`: list of agent ids allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
||||||
|
- Sandbox inheritance guard: if the requester session is sandboxed, `sessions_spawn` rejects targets that would run unsandboxed.
|
||||||
|
|
||||||
Discovery:
|
Discovery:
|
||||||
|
|
||||||
|
|||||||
@@ -1207,6 +1207,7 @@ scripts/sandbox-browser-setup.sh # optional browser image
|
|||||||
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
|
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
|
||||||
- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.
|
- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.
|
||||||
- `subagents.allowAgents`: allowlist of agent ids for `sessions_spawn` (`["*"]` = any; default: same agent only).
|
- `subagents.allowAgents`: allowlist of agent ids for `sessions_spawn` (`["*"]` = any; default: same agent only).
|
||||||
|
- Sandbox inheritance guard: if the requester session is sandboxed, `sessions_spawn` rejects targets that would run unsandboxed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ See [Configuration Reference](/gateway/configuration-reference) and [Slash comma
|
|||||||
Allowlist:
|
Allowlist:
|
||||||
|
|
||||||
- `agents.list[].subagents.allowAgents`: list of agent ids that can be targeted via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
- `agents.list[].subagents.allowAgents`: list of agent ids that can be targeted via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
||||||
|
- Sandbox inheritance guard: if the requester session is sandboxed, `sessions_spawn` rejects targets that would run unsandboxed.
|
||||||
|
|
||||||
Discovery:
|
Discovery:
|
||||||
|
|
||||||
|
|||||||
@@ -154,4 +154,41 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => {
|
|||||||
acceptedAt: 5200,
|
acceptedAt: 5200,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forbids sandboxed cross-agent spawns that would unsandbox the child", async () => {
|
||||||
|
setSessionsSpawnConfigOverride({
|
||||||
|
session: {
|
||||||
|
mainKey: "main",
|
||||||
|
scope: "per-sender",
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
sandbox: {
|
||||||
|
mode: "all",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "main",
|
||||||
|
subagents: {
|
||||||
|
allowAgents: ["research"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "research",
|
||||||
|
sandbox: {
|
||||||
|
mode: "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeSpawn("call11", "research");
|
||||||
|
const details = result.details as { status?: string; error?: string };
|
||||||
|
|
||||||
|
expect(details.status).toBe("forbidden");
|
||||||
|
expect(details.error).toContain("Sandboxed sessions cannot spawn unsandboxed subagents.");
|
||||||
|
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { normalizeDeliveryContext } from "../utils/delivery-context.js";
|
|||||||
import { resolveAgentConfig } from "./agent-scope.js";
|
import { resolveAgentConfig } from "./agent-scope.js";
|
||||||
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
|
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
|
||||||
import { resolveSubagentSpawnModelSelection } from "./model-selection.js";
|
import { resolveSubagentSpawnModelSelection } from "./model-selection.js";
|
||||||
|
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
|
||||||
import { buildSubagentSystemPrompt } from "./subagent-announce.js";
|
import { buildSubagentSystemPrompt } from "./subagent-announce.js";
|
||||||
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||||
import { countActiveRunsForSession, registerSubagentRun } from "./subagent-registry.js";
|
import { countActiveRunsForSession, registerSubagentRun } from "./subagent-registry.js";
|
||||||
@@ -269,6 +270,21 @@ export async function spawnSubagentDirect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
||||||
|
const requesterRuntime = resolveSandboxRuntimeStatus({
|
||||||
|
cfg,
|
||||||
|
sessionKey: requesterInternalKey,
|
||||||
|
});
|
||||||
|
const childRuntime = resolveSandboxRuntimeStatus({
|
||||||
|
cfg,
|
||||||
|
sessionKey: childSessionKey,
|
||||||
|
});
|
||||||
|
if (requesterRuntime.sandboxed && !childRuntime.sandboxed) {
|
||||||
|
return {
|
||||||
|
status: "forbidden",
|
||||||
|
error:
|
||||||
|
"Sandboxed sessions cannot spawn unsandboxed subagents. Set a sandboxed target agent or use the same agent runtime.",
|
||||||
|
};
|
||||||
|
}
|
||||||
const childDepth = callerDepth + 1;
|
const childDepth = callerDepth + 1;
|
||||||
const spawnedByKey = requesterInternalKey;
|
const spawnedByKey = requesterInternalKey;
|
||||||
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
|
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
|
||||||
|
|||||||
Reference in New Issue
Block a user