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); }, );