Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions sdk/typescript/src/trusted-executable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { constants } from "node:fs";
import { access, realpath, stat } from "node:fs/promises";
import { delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
import {
basename,
delimiter,
dirname,
isAbsolute,
join,
relative,
resolve,
sep,
} from "node:path";

export interface TrustedExecutable {
executable: string;
Expand Down Expand Up @@ -65,7 +74,18 @@ export async function resolveTrustedExecutable(
process.platform === "win32" ? constants.F_OK : constants.X_OK,
);
if (!(await stat(canonical)).isFile()) continue;
executable ??= pathLike ? canonical : current.path;
const canonicalParent = await realpath(dirname(current.path)).catch(
() => null,
);
const invocationPath =
canonicalParent === null
? current.path
: join(canonicalParent, basename(current.path));
// Explicit launchers outside the protected root retain invocation semantics
// such as Python virtualenv selection. Repository-local links still execute
// only the canonical target that passed the trust check.
executable ??=
pathLike && isWithin(root, invocationPath) ? canonical : current.path;
} catch {
continue;
}
Expand Down
30 changes: 29 additions & 1 deletion sdk/typescript/tests-ts/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4071,6 +4071,34 @@ describe("runtime directories and plugin Python boundary", () => {
).rejects.toThrow(PluginPythonUnavailableError);
});

testPosix("preserves an explicit virtualenv Python launcher", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const systemBin = join(root, "system", "bin");
const virtualenvBin = join(root, "venv", "bin");
const systemPython = join(systemBin, "python3");
const virtualenvPython = join(virtualenvBin, "python");
await Promise.all([
mkdir(repository),
mkdir(systemBin, { recursive: true }),
mkdir(virtualenvBin, { recursive: true }),
]);
await writeFile(
systemPython,
'#!/bin/sh\ncase "$0" in */venv/bin/python) ;; *) exit 1 ;; esac\nprintf "codex-security-python-ok\\n"\n',
);
await chmod(systemPython, 0o700);
await symlink(systemPython, virtualenvPython);

await expect(
resolvePluginPython({
configuredPath: virtualenvPython,
environment: { PATH: "" },
protectedRoot: repository,
}),
).resolves.toBe(virtualenvPython);
});

testPosix(
"does not load repository-controlled Python startup code",
async () => {
Expand Down Expand Up @@ -4100,7 +4128,7 @@ describe("runtime directories and plugin Python boundary", () => {
environment,
protectedRoot: repository,
}),
).toBe(await realpath(interpreter));
).toBe(interpreter);
expect(existsSync(marker)).toBe(false);
},
);
Expand Down
29 changes: 29 additions & 0 deletions sdk/typescript/tests-ts/trusted-executable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,35 @@ describe("trusted executable resolution", () => {
},
);

test.skipIf(process.platform === "win32")(
"canonicalizes aliased parents before preserving explicit launchers",
async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const repositoryBin = join(repository, "bin");
const aliasedBin = join(root, "repository-bin-alias");
const trusted = join(root, "trusted");
const wrapper = join(trusted, "python3");
const launcher = join(repositoryBin, "python");
await Promise.all([
mkdir(repositoryBin, { recursive: true }),
mkdir(trusted),
]);
await writeFile(wrapper, "#!/bin/sh\nexit 0\n");
await chmod(wrapper, 0o700);
await symlink(wrapper, launcher);
await symlink(repositoryBin, aliasedBin, "dir");

await expect(
resolveTrustedExecutable(
join(aliasedBin, "python"),
{ PATH: "" },
repository,
),
).resolves.toEqual({ executable: wrapper, environment: { PATH: "" } });
},
);

test("selects runnable Windows executables ahead of extensionless and batch files", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
Expand Down