From b4b68e50c4be61c901740ce5c4b708b4d74844cc Mon Sep 17 00:00:00 2001 From: Dipesh Babu Date: Mon, 3 Aug 2026 03:10:58 -0400 Subject: [PATCH 1/2] Preserve explicit Python virtualenv launchers --- sdk/typescript/src/trusted-executable.ts | 6 ++++- sdk/typescript/tests-ts/runtime.test.ts | 30 +++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/trusted-executable.ts b/sdk/typescript/src/trusted-executable.ts index e45b1bd4..7d2948c5 100644 --- a/sdk/typescript/src/trusted-executable.ts +++ b/sdk/typescript/src/trusted-executable.ts @@ -65,7 +65,11 @@ export async function resolveTrustedExecutable( process.platform === "win32" ? constants.F_OK : constants.X_OK, ); if (!(await stat(canonical)).isFile()) continue; - executable ??= pathLike ? canonical : 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, current.path) ? canonical : current.path; } catch { continue; } diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index d3c2fa3f..949c0e7e 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -2809,6 +2809,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 () => { @@ -2838,7 +2866,7 @@ describe("runtime directories and plugin Python boundary", () => { environment, protectedRoot: repository, }), - ).toBe(await realpath(interpreter)); + ).toBe(interpreter); expect(existsSync(marker)).toBe(false); }, ); From 932970e8bfea2eb9f6d3317457fadecb0d7962f1 Mon Sep 17 00:00:00 2001 From: Dipesh Babu Date: Thu, 6 Aug 2026 18:02:47 -0400 Subject: [PATCH 2/2] Canonicalize aliased launcher parents --- sdk/typescript/src/trusted-executable.ts | 20 +++++++++++-- .../tests-ts/trusted-executable.test.ts | 29 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/trusted-executable.ts b/sdk/typescript/src/trusted-executable.ts index 7d2948c5..9c6b115d 100644 --- a/sdk/typescript/src/trusted-executable.ts +++ b/sdk/typescript/src/trusted-executable.ts @@ -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; @@ -65,11 +74,18 @@ export async function resolveTrustedExecutable( process.platform === "win32" ? constants.F_OK : constants.X_OK, ); if (!(await stat(canonical)).isFile()) continue; + 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, current.path) ? canonical : current.path; + pathLike && isWithin(root, invocationPath) ? canonical : current.path; } catch { continue; } diff --git a/sdk/typescript/tests-ts/trusted-executable.test.ts b/sdk/typescript/tests-ts/trusted-executable.test.ts index 34db4501..c6933139 100644 --- a/sdk/typescript/tests-ts/trusted-executable.test.ts +++ b/sdk/typescript/tests-ts/trusted-executable.test.ts @@ -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");