refactor(codex): require PathUri sandbox inputs (#108772)

* refactor(codex): require PathUri sandbox inputs

* test(codex): use PathUri lifecycle fixture
This commit is contained in:
Peter Steinberger
2026-07-16 01:46:39 -07:00
committed by GitHub
parent f771aa54e8
commit 30f8146b38
4 changed files with 38 additions and 44 deletions
@@ -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");
@@ -47,7 +47,7 @@ function processStartParams(processId: string) {
return {
processId,
argv: ["sh", "-lc", "true"],
cwd: "/workspace",
cwd: "file:///workspace",
env: {},
tty: false,
pipeStdin: false,
@@ -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,
@@ -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.`);
}