Skip to content

Commit b26d4c3

Browse files
fix(windows): preserve sandbox override compatibility (#96)
* fix: preserve Windows sandbox override compatibility * fix: preserve Windows sandbox overrides in profiles
1 parent 1a9fc9b commit b26d4c3

2 files changed

Lines changed: 244 additions & 0 deletions

File tree

sdk/typescript/src/config.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,40 @@ export async function mergedCodexConfig(
7272
const overrides = cloneJson(config.codexOverrides ?? {});
7373
validateOverrides(overrides);
7474
validateNativeMultiAgentV2Overrides(overrides);
75+
normalizeLegacyWindowsSandboxOverride(overrides);
76+
const profiles = overrides["profiles"];
77+
if (isObject(profiles)) {
78+
for (const profile of Object.values(profiles)) {
79+
if (isObject(profile)) {
80+
normalizeLegacyWindowsSandboxOverride(profile);
81+
}
82+
}
83+
}
7584
return deepMerge(cloneJson(DEFAULT_CODEX_CONFIG), overrides);
7685
}
7786

87+
function normalizeLegacyWindowsSandboxOverride(overrides: JsonObject): void {
88+
const features = overrides["features"];
89+
if (!isObject(features)) {
90+
return;
91+
}
92+
const elevated = features["elevated_windows_sandbox"];
93+
if (typeof elevated !== "boolean") {
94+
return;
95+
}
96+
const windows = overrides["windows"];
97+
if (
98+
windows !== undefined &&
99+
(!isObject(windows) || Object.hasOwn(windows, "sandbox"))
100+
) {
101+
return;
102+
}
103+
overrides["windows"] = {
104+
...(isObject(windows) ? windows : {}),
105+
sandbox: elevated ? "elevated" : "unelevated",
106+
};
107+
}
108+
78109
export async function writeCodexConfig(
79110
path: string,
80111
config: JsonObject,

sdk/typescript/tests-ts/config.test.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import { afterEach, describe, expect, test } from "bun:test";
55
import { parse } from "smol-toml";
6+
import { scanRuntimeCodexConfig } from "../src/api.js";
67
import {
78
ConfigurationError,
89
DEFAULT_CODEX_CONFIG,
10+
type JsonObject,
911
mergedCodexConfig,
1012
writeCodexConfig,
1113
} from "../src/index.js";
@@ -26,6 +28,39 @@ async function temporaryDirectory(): Promise<string> {
2628
return path;
2729
}
2830

31+
function runPinnedCodex(codexHome: string, arguments_: readonly string[]) {
32+
const node = Bun.which("node");
33+
if (node === null) {
34+
throw new Error("The pinned Codex CLI requires Node.js.");
35+
}
36+
const environment: NodeJS.ProcessEnv = {
37+
...process.env,
38+
CODEX_HOME: codexHome,
39+
};
40+
delete environment["OPENAI_API_KEY"];
41+
delete environment["CODEX_API_KEY"];
42+
return Bun.spawnSync(
43+
[
44+
node,
45+
join(
46+
import.meta.dir,
47+
"..",
48+
"node_modules",
49+
"@openai",
50+
"codex",
51+
"bin",
52+
"codex.js",
53+
),
54+
...arguments_,
55+
],
56+
{
57+
env: environment,
58+
stdout: "pipe",
59+
stderr: "pipe",
60+
},
61+
);
62+
}
63+
2964
describe("Codex configuration", () => {
3065
test("deep-merges native multi-agent v2 defaults", async () => {
3166
const merged = await mergedCodexConfig({
@@ -49,6 +84,183 @@ describe("Codex configuration", () => {
4984
expect(merged["windows"]).toEqual({ sandbox: "elevated" });
5085
});
5186

87+
test("preserves legacy elevated Windows sandbox overrides", async () => {
88+
const merged = await mergedCodexConfig({
89+
codexOverrides: {
90+
features: { elevated_windows_sandbox: true },
91+
},
92+
});
93+
94+
expect(merged).toMatchObject({
95+
features: { elevated_windows_sandbox: true },
96+
windows: { sandbox: "elevated" },
97+
});
98+
});
99+
100+
test("projects legacy elevated Windows overrides into selected profiles", async () => {
101+
const merged = await mergedCodexConfig({
102+
codexOverrides: {
103+
profile: "elevated",
104+
profiles: {
105+
elevated: {
106+
features: { elevated_windows_sandbox: true },
107+
},
108+
},
109+
},
110+
});
111+
112+
expect(merged).toMatchObject({
113+
windows: { sandbox: "unelevated" },
114+
profiles: {
115+
elevated: {
116+
features: { elevated_windows_sandbox: true },
117+
windows: { sandbox: "elevated" },
118+
},
119+
},
120+
});
121+
});
122+
123+
test("allows selected profiles to override root elevated sandbox defaults", async () => {
124+
const merged = await mergedCodexConfig({
125+
codexOverrides: {
126+
features: { elevated_windows_sandbox: true },
127+
profile: "restricted",
128+
profiles: {
129+
restricted: {
130+
features: { elevated_windows_sandbox: false },
131+
},
132+
},
133+
},
134+
});
135+
136+
expect(merged).toMatchObject({
137+
windows: { sandbox: "elevated" },
138+
profiles: {
139+
restricted: {
140+
features: { elevated_windows_sandbox: false },
141+
windows: { sandbox: "unelevated" },
142+
},
143+
},
144+
});
145+
});
146+
147+
test("gives profile-local Windows sandbox overrides precedence", async () => {
148+
const merged = await mergedCodexConfig({
149+
codexOverrides: {
150+
profile: "restricted",
151+
profiles: {
152+
restricted: {
153+
features: { elevated_windows_sandbox: true },
154+
windows: { sandbox: "unelevated" },
155+
},
156+
},
157+
},
158+
});
159+
160+
expect(merged).toMatchObject({
161+
profiles: {
162+
restricted: {
163+
features: { elevated_windows_sandbox: true },
164+
windows: { sandbox: "unelevated" },
165+
},
166+
},
167+
});
168+
});
169+
170+
test("gives explicit Windows sandbox overrides precedence", async () => {
171+
const merged = await mergedCodexConfig({
172+
codexOverrides: {
173+
features: { elevated_windows_sandbox: true },
174+
windows: { sandbox: "unelevated" },
175+
},
176+
});
177+
178+
expect(merged).toMatchObject({
179+
features: { elevated_windows_sandbox: true },
180+
windows: { sandbox: "unelevated" },
181+
});
182+
});
183+
184+
test("retains the Windows sandbox in the hardened scan profile", async () => {
185+
const stateDirectory = join(tmpdir(), "codex-security-windows-state");
186+
const merged = await mergedCodexConfig({});
187+
188+
expect(scanRuntimeCodexConfig(merged, stateDirectory)).toMatchObject({
189+
windows: { sandbox: "unelevated" },
190+
default_permissions: "codex_security_scan",
191+
permissions: {
192+
codex_security_scan: {
193+
filesystem: {
194+
":root": "read",
195+
":workspace_roots": "write",
196+
[stateDirectory]: "write",
197+
},
198+
},
199+
},
200+
});
201+
});
202+
203+
test("writes Windows sandbox settings accepted by the pinned Codex CLI", async () => {
204+
const root = await temporaryDirectory();
205+
const path = join(root, "config.toml");
206+
await writeCodexConfig(path, await mergedCodexConfig({}));
207+
208+
expect(parse(await readFile(path, "utf8"))).toMatchObject({
209+
windows: { sandbox: "unelevated" },
210+
});
211+
212+
const result = runPinnedCodex(root, ["features", "list"]);
213+
expect(result.exitCode).toBe(0);
214+
expect(result.stdout.length).toBeGreaterThan(0);
215+
});
216+
217+
test("writes selected profile sandbox settings accepted by the pinned Codex CLI", async () => {
218+
const root = await temporaryDirectory();
219+
const path = join(root, "config.toml");
220+
const config = await mergedCodexConfig({
221+
codexOverrides: {
222+
profile: "elevated",
223+
profiles: {
224+
elevated: {
225+
features: { elevated_windows_sandbox: true },
226+
},
227+
},
228+
},
229+
});
230+
const nativeConfig = structuredClone(config);
231+
delete nativeConfig["profile"];
232+
delete nativeConfig["profiles"];
233+
const profileConfig = (config["profiles"] as JsonObject)[
234+
"elevated"
235+
] as JsonObject;
236+
const profilePath = join(root, "elevated.config.toml");
237+
await writeCodexConfig(path, nativeConfig);
238+
await writeCodexConfig(profilePath, profileConfig);
239+
240+
expect(parse(await readFile(path, "utf8"))).toMatchObject({
241+
windows: { sandbox: "unelevated" },
242+
});
243+
expect(parse(await readFile(profilePath, "utf8"))).toMatchObject({
244+
features: { elevated_windows_sandbox: true },
245+
windows: { sandbox: "elevated" },
246+
});
247+
248+
const result = runPinnedCodex(root, [
249+
"--profile",
250+
"elevated",
251+
"mcp",
252+
"list",
253+
"--json",
254+
]);
255+
if (result.exitCode !== 0) {
256+
throw new Error(
257+
`The pinned Codex CLI rejected the selected Windows sandbox profile: ${new TextDecoder().decode(result.stderr)}`,
258+
);
259+
}
260+
expect(result.exitCode).toBe(0);
261+
expect(result.stdout.length).toBeGreaterThan(0);
262+
});
263+
52264
test("rejects prototype-bearing override keys", async () => {
53265
for (const key of ["__proto__", "constructor", "prototype"]) {
54266
await expect(
@@ -73,6 +285,7 @@ describe("Codex configuration", () => {
73285
test("keeps exported default configuration deeply immutable", async () => {
74286
expect(Object.isFrozen(DEFAULT_CODEX_CONFIG)).toBe(true);
75287
expect(Object.isFrozen(DEFAULT_CODEX_CONFIG["features"])).toBe(true);
288+
expect(Object.isFrozen(DEFAULT_CODEX_CONFIG["windows"])).toBe(true);
76289
expect(
77290
Object.isFrozen(
78291
(DEFAULT_CODEX_CONFIG["features"] as Record<string, unknown>)[

0 commit comments

Comments
 (0)