Skip to content

Commit 389b46e

Browse files
Jefskycursoragent
andcommitted
fix(config): honor SPAWN_ALLOWLIST for sessions_spawn (#79490)
Reads OPENCLAW_SPAWN_ALLOWLIST then SPAWN_ALLOWLIST and applies them to agents.defaults.subagents.allowAgents during config load (env wins). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9931caf commit 389b46e

3 files changed

Lines changed: 116 additions & 0 deletions

File tree

src/config/io.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ import {
101101
type RuntimeConfigWriteNotification,
102102
} from "./runtime-snapshot.js";
103103
import { resolveShellEnvExpectedKeys } from "./shell-env-expected-keys.js";
104+
import { applySpawnAllowlistEnvOverlay } from "./spawn-allowlist-env.js";
104105
import type { OpenClawConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
105106
import {
106107
validateConfigObjectRawWithPlugins,
@@ -1242,6 +1243,7 @@ export function createConfigIO(
12421243
}
12431244

12441245
applyConfigEnvVars(cfg, deps.env);
1246+
applySpawnAllowlistEnvOverlay(cfg, deps.env);
12451247

12461248
const enabled = shouldEnableShellEnvFallback(deps.env) || cfg.env?.shellEnv?.enabled === true;
12471249
if (enabled && !shouldDeferShellEnvFallback(deps.env)) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
applySpawnAllowlistEnvOverlay,
4+
resolveSpawnAllowlistFromProcessEnv,
5+
} from "./spawn-allowlist-env.js";
6+
import type { OpenClawConfig } from "./types.openclaw.js";
7+
8+
describe("resolveSpawnAllowlistFromProcessEnv", () => {
9+
it("reads OPENCLAW_SPAWN_ALLOWLIST", () => {
10+
expect(resolveSpawnAllowlistFromProcessEnv({ OPENCLAW_SPAWN_ALLOWLIST: "*" })).toEqual(["*"]);
11+
});
12+
13+
it("falls back to SPAWN_ALLOWLIST", () => {
14+
expect(resolveSpawnAllowlistFromProcessEnv({ SPAWN_ALLOWLIST: "*" })).toEqual(["*"]);
15+
});
16+
17+
it("parses comma-separated ids", () => {
18+
expect(resolveSpawnAllowlistFromProcessEnv({ SPAWN_ALLOWLIST: " alpha, beta " })).toEqual([
19+
"alpha",
20+
"beta",
21+
]);
22+
});
23+
24+
it("parses JSON string arrays", () => {
25+
expect(resolveSpawnAllowlistFromProcessEnv({ SPAWN_ALLOWLIST: '["a","b"]' })).toEqual([
26+
"a",
27+
"b",
28+
]);
29+
});
30+
31+
it("prefers OPENCLAW_ over unprefixed", () => {
32+
expect(
33+
resolveSpawnAllowlistFromProcessEnv({
34+
OPENCLAW_SPAWN_ALLOWLIST: "fast",
35+
SPAWN_ALLOWLIST: "slow",
36+
}),
37+
).toEqual(["fast"]);
38+
});
39+
});
40+
41+
describe("applySpawnAllowlistEnvOverlay", () => {
42+
it("writes agents.defaults.subagents.allowAgents when env set", () => {
43+
const cfg = {} as OpenClawConfig;
44+
applySpawnAllowlistEnvOverlay(cfg, { SPAWN_ALLOWLIST: "*" });
45+
expect(cfg.agents?.defaults?.subagents?.allowAgents).toEqual(["*"]);
46+
});
47+
48+
it("overwrites existing config allowAgents when env set (Docker precedence)", () => {
49+
const cfg = {
50+
agents: { defaults: { subagents: { allowAgents: ["legacy"] } } },
51+
} as OpenClawConfig;
52+
applySpawnAllowlistEnvOverlay(cfg, { SPAWN_ALLOWLIST: "x,y" });
53+
expect(cfg.agents?.defaults?.subagents?.allowAgents).toEqual(["x", "y"]);
54+
});
55+
56+
it("noop when unset", () => {
57+
const cfg = { agents: { defaults: {} } } as OpenClawConfig;
58+
applySpawnAllowlistEnvOverlay(cfg, {});
59+
expect(cfg.agents?.defaults?.subagents?.allowAgents).toBeUndefined();
60+
});
61+
});

src/config/spawn-allowlist-env.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { normalizeOptionalString } from "../shared/string-coerce.js";
2+
import type { OpenClawConfig } from "./types.openclaw.js";
3+
4+
/**
5+
* Parses `OPENCLAW_SPAWN_ALLOWLIST` or `SPAWN_ALLOWLIST` for Docker deployments (#79490).
6+
* When set, applies to `agents.defaults.subagents.allowAgents` unless already configured on disk —
7+
* callers should only invoke the overlay path when JSON did not specify `allowAgents`.
8+
*/
9+
export function resolveSpawnAllowlistFromProcessEnv(env: NodeJS.ProcessEnv): string[] | undefined {
10+
const raw =
11+
normalizeOptionalString(env.OPENCLAW_SPAWN_ALLOWLIST) ??
12+
normalizeOptionalString(env.SPAWN_ALLOWLIST);
13+
if (!raw) {
14+
return undefined;
15+
}
16+
const trimmed = raw.trim();
17+
if (!trimmed) {
18+
return undefined;
19+
}
20+
if (trimmed === "*") {
21+
return ["*"];
22+
}
23+
if (trimmed.startsWith("[")) {
24+
try {
25+
const parsed = JSON.parse(trimmed) as unknown;
26+
if (Array.isArray(parsed)) {
27+
const ids = parsed
28+
.filter((entry): entry is string => typeof entry === "string")
29+
.map((entry) => entry.trim())
30+
.filter(Boolean);
31+
return ids.length > 0 ? ids : undefined;
32+
}
33+
} catch {
34+
// Fall through to comma-separated parsing for mis-quoted JSON-ish values.
35+
}
36+
}
37+
const split = trimmed
38+
.split(",")
39+
.map((segment) => segment.trim())
40+
.filter(Boolean);
41+
return split.length > 0 ? split : undefined;
42+
}
43+
44+
export function applySpawnAllowlistEnvOverlay(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): void {
45+
const fromEnv = resolveSpawnAllowlistFromProcessEnv(env);
46+
if (!fromEnv) {
47+
return;
48+
}
49+
cfg.agents ??= {};
50+
cfg.agents.defaults ??= {};
51+
cfg.agents.defaults.subagents ??= {};
52+
cfg.agents.defaults.subagents.allowAgents = fromEnv;
53+
}

0 commit comments

Comments
 (0)