From 30f8146b383d9dffcac153554f2a22e23e2d3fa3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 16 Jul 2026 01:46:39 -0700 Subject: [PATCH] refactor(codex): require PathUri sandbox inputs (#108772) * refactor(codex): require PathUri sandbox inputs * test(codex): use PathUri lifecycle fixture --- .../app-server/sandbox-exec-server.fs.test.ts | 48 +++++++++---------- .../sandbox-exec-server.lifecycle.test.ts | 2 +- .../app-server/sandbox-exec-server.test.ts | 12 +++-- .../sandbox-exec-server/path-uri.ts | 20 +++----- 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/extensions/codex/src/app-server/sandbox-exec-server.fs.test.ts b/extensions/codex/src/app-server/sandbox-exec-server.fs.test.ts index 463b6cc26e7e..2b866666a361 100644 --- a/extensions/codex/src/app-server/sandbox-exec-server.fs.test.ts +++ b/extensions/codex/src/app-server/sandbox-exec-server.fs.test.ts @@ -71,7 +71,7 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/writeFile", { - path: "/workspace/missing/note.txt", + path: "file:///workspace/missing/note.txt", dataBase64: Buffer.from("hello").toString("base64"), }), ).rejects.toThrow("parent directory not found"); @@ -94,7 +94,7 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/writeFile", { - path: "/workspace/read-only.txt", + path: "file:///workspace/read-only.txt", dataBase64: Buffer.from("blocked").toString("base64"), sandbox: codexFsSandboxContext({ entries: [{ path: specialPath("root"), access: "read" }], @@ -102,7 +102,7 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { }), ).rejects.toThrow("Codex fs sandbox denied write access"); await rpc(socket, "fs/writeFile", { - path: "/workspace/allowed.txt", + path: "file:///workspace/allowed.txt", dataBase64: Buffer.from("allowed").toString("base64"), sandbox: codexFsSandboxContext({ entries: [ @@ -143,14 +143,14 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/writeFile", { - path: "/workspace/.git/config", + path: "file:///workspace/.git/config", dataBase64: Buffer.from("blocked").toString("base64"), sandbox: workspacePolicy, }), ).rejects.toThrow("Codex fs sandbox denied write access"); await expect( rpc(socket, "fs/remove", { - path: "/workspace", + path: "file:///workspace", recursive: true, force: true, sandbox: workspacePolicy, @@ -185,13 +185,13 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/readFile", { - path: "/workspace/private/secret.txt", + path: "file:///workspace/private/secret.txt", sandbox: policy, }), ).rejects.toThrow("Codex fs sandbox denied read access"); await expect( rpc(socket, "fs/readFile", { - path: "/workspace/key.pem", + path: "file:///workspace/key.pem", sandbox: codexFsSandboxContext({ entries: [ { path: specialPath("root"), access: "read" }, @@ -203,7 +203,7 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { ).rejects.toThrow("Codex fs sandbox denied read access"); await expect( rpc(socket, "fs/readFile", { - path: "/workspace/KEY.PEM", + path: "file:///workspace/KEY.PEM", sandbox: codexFsSandboxContext({ entries: [ { path: specialPath("root"), access: "read" }, @@ -214,13 +214,13 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { }), ).rejects.toThrow("Codex fs sandbox denied read access"); await rpc(socket, "fs/writeFile", { - path: "/workspace/private/nested/allowed.txt", + path: "file:///workspace/private/nested/allowed.txt", dataBase64: Buffer.from("ok").toString("base64"), sandbox: policy, }); await expect( rpc(socket, "fs/remove", { - path: "/workspace/private", + path: "file:///workspace/private", recursive: true, force: true, sandbox: policy, @@ -246,7 +246,7 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { socket.send(JSON.stringify({ method: "initialized" })); await rpc(socket, "fs/writeFile", { - path: "/workspace/allowed.txt", + path: "file:///workspace/allowed.txt", dataBase64: Buffer.from("ok").toString("base64"), sandbox: codexFsSandboxContext({ entries: [ @@ -279,7 +279,7 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/readFile", { - path: "/workspace/key.pem", + path: "file:///workspace/key.pem", sandbox: codexFsSandboxContext({ entries: [ { path: specialPath("root"), access: "read" }, @@ -315,7 +315,7 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/remove", { - path: "/workspace/src", + path: "file:///workspace/src", recursive: true, force: true, sandbox: policy, @@ -414,8 +414,8 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/copy", { - sourcePath: "/workspace/source-dir", - destinationPath: "/workspace/source-dir/backup", + sourcePath: "file:///workspace/source-dir", + destinationPath: "file:///workspace/source-dir/backup", recursive: true, }), ).rejects.toThrow("Cannot recursively copy a directory into itself"); @@ -435,9 +435,9 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await rpc(socket, "initialize", { clientName: "test" }); socket.send(JSON.stringify({ method: "initialized" })); - await expect(rpc(socket, "fs/getMetadata", { path: "/workspace/missing" })).rejects.toThrow( - "file not found", - ); + await expect( + rpc(socket, "fs/getMetadata", { path: "file:///workspace/missing" }), + ).rejects.toThrow("file not found"); socket.close(); }); @@ -460,9 +460,9 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await rpc(socket, "initialize", { clientName: "test" }); socket.send(JSON.stringify({ method: "initialized" })); - await expect(rpc(socket, "fs/readFile", { path: "/workspace/huge.bin" })).rejects.toThrow( - "file is too large to read through Codex sandbox exec-server", - ); + await expect( + rpc(socket, "fs/readFile", { path: "file:///workspace/huge.bin" }), + ).rejects.toThrow("file is too large to read through Codex sandbox exec-server"); expect(readFile).not.toHaveBeenCalled(); socket.close(); @@ -486,14 +486,14 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/createDirectory", { - path: "/workspace/missing/child", + path: "file:///workspace/missing/child", recursive: false, }), ).rejects.toThrow("parent directory not found"); expect(mkdirp).not.toHaveBeenCalled(); await rpc(socket, "fs/createDirectory", { - path: "/workspace/existing/child", + path: "file:///workspace/existing/child", recursive: false, }); expect(mkdirp).toHaveBeenCalledWith({ filePath: "/workspace/existing/child" }); @@ -517,7 +517,7 @@ describe("OpenClaw Codex sandbox exec-server filesystem", () => { await expect( rpc(socket, "fs/writeFile", { - path: "/outside/note.txt", + path: "file:///outside/note.txt", dataBase64: Buffer.from("no").toString("base64"), }), ).rejects.toThrow("sandbox denied write outside workspace"); diff --git a/extensions/codex/src/app-server/sandbox-exec-server.lifecycle.test.ts b/extensions/codex/src/app-server/sandbox-exec-server.lifecycle.test.ts index 2a49b9e0ea98..e1882f7b5e5a 100644 --- a/extensions/codex/src/app-server/sandbox-exec-server.lifecycle.test.ts +++ b/extensions/codex/src/app-server/sandbox-exec-server.lifecycle.test.ts @@ -47,7 +47,7 @@ function processStartParams(processId: string) { return { processId, argv: ["sh", "-lc", "true"], - cwd: "/workspace", + cwd: "file:///workspace", env: {}, tty: false, pipeStdin: false, diff --git a/extensions/codex/src/app-server/sandbox-exec-server.test.ts b/extensions/codex/src/app-server/sandbox-exec-server.test.ts index e6dcfb589097..06910740f6e1 100644 --- a/extensions/codex/src/app-server/sandbox-exec-server.test.ts +++ b/extensions/codex/src/app-server/sandbox-exec-server.test.ts @@ -213,6 +213,8 @@ describe("OpenClaw Codex sandbox exec-server", () => { }); it.each([ + ["a native absolute path", "/workspace"], + ["a relative path", "workspace"], ["a non-file scheme", "https://example.test/workspace"], ["a remote file authority", "file://remote.example.test/workspace"], ["a query", "file:///workspace?revision=1"], @@ -287,7 +289,7 @@ describe("OpenClaw Codex sandbox exec-server", () => { rpc(socket, "process/start", { processId: "proc-arg0", argv: ["/bin/sh", "-lc", "true"], - cwd: "/workspace", + cwd: "file:///workspace", env: {}, tty: false, pipeStdin: false, @@ -318,7 +320,7 @@ describe("OpenClaw Codex sandbox exec-server", () => { await rpc(socket, "process/start", { processId: "proc-stdin", argv: ["/bin/sh", "-lc", "cat"], - cwd: "/workspace", + cwd: "file:///workspace", env: {}, tty: false, pipeStdin: true, @@ -356,7 +358,7 @@ describe("OpenClaw Codex sandbox exec-server", () => { await rpc(socket, "process/start", { processId: "proc-tty", argv: ["/bin/sh", "-lc", "cat"], - cwd: "/workspace", + cwd: "file:///workspace", env: {}, tty: true, pipeStdin: false, @@ -403,7 +405,7 @@ describe("OpenClaw Codex sandbox exec-server", () => { await rpc(socket, "process/start", { processId: "proc-secret-env", argv: ["/bin/sh", "-lc", "true"], - cwd: "/workspace", + cwd: "file:///workspace", env: {}, envPolicy: { inherit: "all", @@ -449,7 +451,7 @@ describe("OpenClaw Codex sandbox exec-server", () => { await rpc(socket, "process/start", { processId: "proc-cursor", argv: [process.execPath, "-e", "ignored"], - cwd: "/workspace", + cwd: "file:///workspace", env: {}, tty: false, pipeStdin: false, diff --git a/extensions/codex/src/app-server/sandbox-exec-server/path-uri.ts b/extensions/codex/src/app-server/sandbox-exec-server/path-uri.ts index 20a7db69794b..fc42cd86abec 100644 --- a/extensions/codex/src/app-server/sandbox-exec-server/path-uri.ts +++ b/extensions/codex/src/app-server/sandbox-exec-server/path-uri.ts @@ -1,23 +1,10 @@ /** Converts Codex PathUri protocol values into sandbox-backend path strings. */ import { fileURLToPath } from "node:url"; -const URI_SCHEME_RE = /^([A-Za-z][A-Za-z0-9+.-]*):/u; -const WINDOWS_ABSOLUTE_PATH_RE = /^[A-Za-z]:[\\/]/u; const WINDOWS_DRIVE_PATH_RE = /^\/[A-Za-z]:(?:\/|$)/u; -/** Resolves one Codex exec-server path while retaining legacy native absolute paths. */ +/** Resolves one Codex exec-server PathUri into a POSIX sandbox path. */ export function resolveExecServerPath(rawPath: string, label: string): string { - const scheme = WINDOWS_ABSOLUTE_PATH_RE.test(rawPath) - ? undefined - : URI_SCHEME_RE.exec(rawPath)?.[1]?.toLowerCase(); - if (!scheme) { - // Codex versions before PathUri sent native absolute paths here. - return rawPath; - } - if (scheme !== "file") { - throw new Error(`${label} URI must use the file scheme, received ${scheme}.`); - } - let pathUrl: URL; try { pathUrl = new URL(rawPath); @@ -27,6 +14,11 @@ export function resolveExecServerPath(rawPath: string, label: string): string { { cause: error }, ); } + if (pathUrl.protocol !== "file:") { + throw new Error( + `${label} URI must use the file scheme, received ${pathUrl.protocol.slice(0, -1)}.`, + ); + } if (pathUrl.search || pathUrl.hash) { throw new Error(`${label} file URI must not include a query or fragment.`); }