Skip to content
Open
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"playwright-core": "1.60.0",
"proper-lockfile": "4.1.2",
"saxes": "6.0.0",
"smol-toml": "1.9.1",
"stream-chain": "^4.2.5",
"stream-json": "3.7.0",
"transcribe-cpp": "0.4.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ describe("CodexAdapterV2 process exit", () => {
),
),
),
Layer.provide(NodeServices.layer),
);
return Effect.gen(function* () {
const factory = yield* CodexAdapterV2.CodexAppServerClientFactory;
Expand Down
20 changes: 19 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,8 @@ export const layerAppServerClientFactory: Layer.Layer<
CodexAppServerClientFactory,
never,
| ChildProcessSpawner.ChildProcessSpawner
| FileSystem.FileSystem
| Path.Path
| PortScanner.PortDiscovery
| ProviderEventLoggers.ProviderEventLoggers
> = Layer.effect(
Expand All @@ -1590,18 +1592,34 @@ export const layerAppServerClientFactory: Layer.Layer<
const { native: nativeEventLogger } = yield* ProviderEventLoggers.ProviderEventLoggers;
const agentScope = yield* AgentScope;
const portDiscovery = yield* PortScanner.PortDiscovery;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;

return CodexAppServerClientFactory.of({
open: Effect.fn("CodexAdapterV2.open")(function* (input) {
const scope = yield* Scope.Scope;
const homeDirectory = yield* HostProcess.HomeDirectory;
const codexHome = input.settings.homePath
? expandHomePath(input.settings.homePath, homeDirectory)
: input.environment.CODEX_HOME || path.join(homeDirectory, ".codex");
const codexConfig = yield* fileSystem
.readFileString(path.join(codexHome, "config.toml"))
.pipe(Effect.orElseSucceed(() => ""));
const { env, configArgs } = codexAppServerEnvironment(
input.environment,
yield* HostProcess.Platform,
codexConfig,
);
const environment = {
...codexAppServerEnvironment(input.environment),
...env,
...(input.settings.homePath ? { CODEX_HOME: input.settings.homePath } : {}),
};
const command = yield* makeCodexAppServerSpawnCommand({
command: input.settings.binaryPath || "codex",
// Spawn-level config reaches the shells of every thread and one-off command.
args: codexAppServerArgs(
resolveCodexLaunchArgs(input.settings.launchArgs, input.environment),
configArgs,
),
env: environment,
threadId: input.threadId,
Expand Down
113 changes: 112 additions & 1 deletion apps/server/src/provider/CodexProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { assert, it } from "@effect/vitest";

import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts";
import {
applyPreferredCodexDefaultModel,
codexAppServerEnvironment,
mapCodexModelCapabilities,
} from "./CodexProvider.ts";

it("maps current Codex model capability fields", () => {
const capabilities = mapCodexModelCapabilities({
Expand Down Expand Up @@ -153,3 +157,110 @@ it("ignores custom models that shadow a preferred slug", () => {

assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4");
});

const PROXY = "http://proxy.corp.example:8080";
const LOOPBACK = "127.0.0.1,localhost,::1";
const BLANK_SHELL_NO_PROXY = [
"-c",
'shell_environment_policy.set.NO_PROXY=""',
"-c",
'shell_environment_policy.set.no_proxy=""',
];

it.each([
{
name: "extends each spelling the user set with only its own hosts",
platform: "linux",
environment: { HTTP_PROXY: PROXY, NO_PROXY: "intranet.example", no_proxy: "remote.example" },
expected: { NO_PROXY: `intranet.example,${LOOPBACK}`, no_proxy: `remote.example,${LOOPBACK}` },
configArgs: [],
},
{
name: "does not create NO_PROXY, which would shadow the user's no_proxy",
platform: "darwin",
environment: { no_proxy: "remote.example" },
expected: { NO_PROXY: undefined, no_proxy: `remote.example,${LOOPBACK}` },
configArgs: [],
},
{
name: "keeps a wildcard as the user wrote it",
platform: "linux",
environment: { HTTPS_PROXY: PROXY, NO_PROXY: "*" },
expected: { NO_PROXY: "*", no_proxy: undefined },
configArgs: [],
},
{
name: "extends an empty NO_PROXY, which Codex reads before no_proxy",
platform: "linux",
environment: { HTTP_PROXY: PROXY, NO_PROXY: "", no_proxy: "intranet.example" },
expected: { NO_PROXY: LOOPBACK, no_proxy: `intranet.example,${LOOPBACK}` },
configArgs: [],
},
{
name: "creates both spellings when Codex already uses env proxies",
platform: "linux",
environment: { https_proxy: PROXY },
expected: { NO_PROXY: LOOPBACK, no_proxy: LOOPBACK },
configArgs: [],
},
{
name: "keeps the Windows registry bypass list in charge alongside env proxies",
platform: "win32",
environment: { https_proxy: PROXY },
expected: { NO_PROXY: undefined, no_proxy: undefined },
configArgs: [],
},
{
name: "extends a Windows NO_PROXY the user already set",
platform: "win32",
environment: { https_proxy: PROXY, NO_PROXY: "intranet.example" },
expected: { NO_PROXY: `intranet.example,${LOOPBACK}`, no_proxy: undefined },
configArgs: [],
},
{
name: "exempts loopback from a macOS system proxy without reaching agent shells",
platform: "darwin",
environment: {},
expected: { NO_PROXY: LOOPBACK, no_proxy: LOOPBACK },
configArgs: BLANK_SHELL_NO_PROXY,
},
{
name: "ignores no_proxy mentioned outside shell_environment_policy.set",
platform: "darwin",
environment: {},
codexConfig:
'# no_proxy is not configured\n[shell_environment_policy]\nexclude = ["no_proxy"]\n',
expected: { NO_PROXY: LOOPBACK, no_proxy: LOOPBACK },
configArgs: BLANK_SHELL_NO_PROXY,
},
{
name: "keeps shell bypasses the user's Codex config sets",
platform: "darwin",
environment: {},
codexConfig: '[shell_environment_policy.set]\nNO_PROXY = "127.0.0.1"\n',
expected: { NO_PROXY: LOOPBACK, no_proxy: LOOPBACK },
configArgs: [],
},
{
name: "leaves Linux without env proxies unchanged",
platform: "linux",
environment: { NO_PROXY: "" },
expected: { NO_PROXY: "", no_proxy: undefined },
configArgs: [],
},
] satisfies ReadonlyArray<{
name: string;
platform: NodeJS.Platform;
environment: NodeJS.ProcessEnv;
codexConfig?: string;
expected: { NO_PROXY: string | undefined; no_proxy: string | undefined };
configArgs: ReadonlyArray<string>;
}>)("$name", ({ platform, environment, codexConfig, expected, configArgs }) => {
const launch = codexAppServerEnvironment(environment, platform, codexConfig);

assert.deepStrictEqual(
{ NO_PROXY: launch.env.NO_PROXY, no_proxy: launch.env.no_proxy },
expected,
);
assert.deepStrictEqual(launch.configArgs, configArgs);
});
77 changes: 70 additions & 7 deletions apps/server/src/provider/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
type CodexResetCreditsSummary,
} from "./codexUsageLimits.ts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { parse as parseToml } from "smol-toml";
import packageJson from "../../package.json" with { type: "json" };
const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError);
const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000;
Expand Down Expand Up @@ -449,13 +450,72 @@ const requestCodexChatGptModels = Effect.fn("requestCodexChatGptModels")(functio
);
});

/** Set originator at spawn; the pre-initialize refresh otherwise overwrites shared cache with plain-CLI models. */
const LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "::1"];

const hasValue = (value: string | undefined): value is string => (value?.trim() ?? "") !== "";

const withLoopback = (value: string) => {
const hosts = value
.split(",")
.map((host) => host.trim())
.filter((host) => host !== "");
return hosts.includes("*") ? value : [...new Set([...hosts, ...LOOPBACK_HOSTS])].join(",");
};

const configSetsShellNoProxy = (codexConfig: string) => {
let config: unknown;
try {
config = parseToml(codexConfig);
} catch {
// Codex refuses an unparseable config, so there are no shells to protect.
return true;
}
const policy = (config as { shell_environment_policy?: unknown }).shell_environment_policy;
const set =
typeof policy === "object" && policy !== null ? (policy as { set?: unknown }).set : {};
return (
typeof set === "object" &&
set !== null &&
Object.keys(set).some((name) => name.toUpperCase() === "NO_PROXY")
);
};

/**
* Set originator at spawn; the pre-initialize refresh otherwise overwrites shared cache with plain-CLI models.
* Exempts loopback from proxies so Codex's MCP client reaches T3's MCP endpoint directly.
*/
export const codexAppServerEnvironment = (
environment: NodeJS.ProcessEnv | undefined,
): NodeJS.ProcessEnv => ({
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: CODEX_CLIENT_NAME,
...environment,
});
environment: NodeJS.ProcessEnv,
platform: NodeJS.Platform,
codexConfig = "",
): { readonly env: NodeJS.ProcessEnv; readonly configArgs: ReadonlyArray<string> } => {
const base = { CODEX_INTERNAL_ORIGINATOR_OVERRIDE: CODEX_CLIENT_NAME, ...environment };
const proxyNames = Object.keys(environment).filter((name) =>
name.toUpperCase().endsWith("_PROXY"),
);
const noProxyNames = proxyNames.filter((name) => name.toUpperCase() === "NO_PROXY");
const usesEnvProxy = proxyNames.some((name) => hasValue(environment[name]));
const inject =
platform === "win32"
? // Windows reads its registry bypass list only while NO_PROXY is empty.
noProxyNames.some((name) => hasValue(environment[name]))
: usesEnvProxy || platform === "darwin";
if (!inject) return { env: base, configArgs: [] };
// Codex reads NO_PROXY, even empty, before no_proxy, so each spelling keeps its own hosts.
const names = noProxyNames.length > 0 ? noProxyNames : ["NO_PROXY", "no_proxy"];
return {
env: {
...base,
...Object.fromEntries(names.map((name) => [name, withLoopback(environment[name] ?? "")])),
},
// Any non-empty *_proxy turns off Python's macOS system proxy, so agent shells get them blank.
// `-c` outranks config.toml, so a config that names either spelling keeps its own values.
configArgs:
usesEnvProxy || configSetsShellNoProxy(codexConfig)
? []
: names.flatMap((name) => ["-c", `shell_environment_policy.set.${name}=""`]),
};
};

export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams {
return {
Expand All @@ -479,7 +539,10 @@ const codexProcessEnvironment = Effect.fn("codexProcessEnvironment")(function* (
? expandHomePath(input.homePath, yield* HostProcess.HomeDirectory)
: undefined;
return {
...codexAppServerEnvironment(input.environment),
...codexAppServerEnvironment(
input.environment ?? (yield* HostProcess.Environment),
yield* HostProcess.Platform,
).env,
...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}),
};
});
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/codexLaunchArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export const resolveCodexLaunchArgs = (

const codexLaunchArgv = (launchArgs?: string): ReadonlyArray<string> => tokenizeCliArgs(launchArgs);

export const codexAppServerArgs = (launchArgs?: string) => [
export const codexAppServerArgs = (launchArgs?: string, configArgs: ReadonlyArray<string> = []) => [
"app-server",
...configArgs,
...codexLaunchArgv(launchArgs),
];

Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading