diff --git a/plugins/codex/scripts/lib/app-server.mjs b/plugins/codex/scripts/lib/app-server.mjs index 72b30a764..8ecacf213 100644 --- a/plugins/codex/scripts/lib/app-server.mjs +++ b/plugins/codex/scripts/lib/app-server.mjs @@ -20,6 +20,7 @@ const PLUGIN_MANIFEST_URL = new URL("../../.claude-plugin/plugin.json", import.m const PLUGIN_MANIFEST = JSON.parse(fs.readFileSync(PLUGIN_MANIFEST_URL, "utf8")); export const BROKER_ENDPOINT_ENV = "CODEX_COMPANION_APP_SERVER_ENDPOINT"; +export const BROKER_DISABLE_ENV = "CODEX_COMPANION_APP_SERVER_DISABLE_BROKER"; export const BROKER_BUSY_RPC_CODE = -32001; /** @type {ClientInfo} */ @@ -332,10 +333,23 @@ class BrokerCodexAppServerClient extends AppServerClientBase { } } +export function resolveDisableBroker(options) { + if (options.disableBroker !== undefined) { + return Boolean(options.disableBroker); + } + const raw = options.env?.[BROKER_DISABLE_ENV] ?? process.env[BROKER_DISABLE_ENV] ?? null; + if (raw === null || raw === undefined) { + return false; + } + const normalized = String(raw).trim().toLowerCase(); + return normalized === "true" || normalized === "1" || normalized === "yes"; +} + export class CodexAppServerClient { static async connect(cwd, options = {}) { + const disableBroker = resolveDisableBroker(options); let brokerEndpoint = null; - if (!options.disableBroker) { + if (!disableBroker) { brokerEndpoint = options.brokerEndpoint ?? options.env?.[BROKER_ENDPOINT_ENV] ?? process.env[BROKER_ENDPOINT_ENV] ?? null; if (!brokerEndpoint && options.reuseExistingBroker) { brokerEndpoint = loadBrokerSession(cwd)?.endpoint ?? null; diff --git a/plugins/codex/scripts/lib/codex.mjs b/plugins/codex/scripts/lib/codex.mjs index fead00cc4..2e260a5e2 100644 --- a/plugins/codex/scripts/lib/codex.mjs +++ b/plugins/codex/scripts/lib/codex.mjs @@ -40,7 +40,7 @@ import os from "node:os"; import path from "node:path"; import { readJsonFile } from "./fs.mjs"; -import { BROKER_BUSY_RPC_CODE, BROKER_ENDPOINT_ENV, CodexAppServerClient } from "./app-server.mjs"; +import { BROKER_BUSY_RPC_CODE, BROKER_ENDPOINT_ENV, CodexAppServerClient, resolveDisableBroker } from "./app-server.mjs"; import { loadBrokerSession } from "./broker-lifecycle.mjs"; import { binaryAvailable } from "./process.mjs"; @@ -904,6 +904,15 @@ export function getCodexAvailability(cwd) { } export function getSessionRuntimeStatus(env = process.env, cwd = process.cwd()) { + if (resolveDisableBroker({ env })) { + return { + mode: "direct", + label: "direct startup", + detail: "Broker discovery is disabled by CODEX_COMPANION_APP_SERVER_DISABLE_BROKER; each command starts its own Codex runtime.", + endpoint: null + }; + } + const endpoint = env?.[BROKER_ENDPOINT_ENV] ?? loadBrokerSession(cwd)?.endpoint ?? null; if (endpoint) { return { diff --git a/tests/app-server.test.mjs b/tests/app-server.test.mjs new file mode 100644 index 000000000..1c4d75029 --- /dev/null +++ b/tests/app-server.test.mjs @@ -0,0 +1,44 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { buildEnv, installFakeCodex } from "./fake-codex-fixture.mjs"; +import { makeTempDir } from "./helpers.mjs"; +import { BROKER_DISABLE_ENV, CodexAppServerClient } from "../plugins/codex/scripts/lib/app-server.mjs"; + +test("CodexAppServerClient.connect spawns directly when CODEX_COMPANION_APP_SERVER_DISABLE_BROKER is true", async () => { + const binDir = makeTempDir(); + installFakeCodex(binDir); + const cwd = makeTempDir(); + const env = { ...buildEnv(binDir), [BROKER_DISABLE_ENV]: "true" }; + + const client = await CodexAppServerClient.connect(cwd, { env }); + + assert.equal(client.transport, "direct"); + await client.close(); +}); + +test("CodexAppServerClient.connect accepts 1 as a truthy disableBroker env value", async () => { + const binDir = makeTempDir(); + installFakeCodex(binDir); + const cwd = makeTempDir(); + const env = { ...buildEnv(binDir), [BROKER_DISABLE_ENV]: "1" }; + + const client = await CodexAppServerClient.connect(cwd, { env }); + + assert.equal(client.transport, "direct"); + await client.close(); +}); + +test("CodexAppServerClient.connect treats an explicit disableBroker option as an override", async () => { + const binDir = makeTempDir(); + installFakeCodex(binDir); + const cwd = makeTempDir(); + // Even though the env var is set, an explicit option wins. + const client = await CodexAppServerClient.connect(cwd, { + env: buildEnv(binDir), + disableBroker: true + }); + + assert.equal(client.transport, "direct"); + await client.close(); +}); diff --git a/tests/codex.test.mjs b/tests/codex.test.mjs new file mode 100644 index 000000000..485b6c70b --- /dev/null +++ b/tests/codex.test.mjs @@ -0,0 +1,50 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { getSessionRuntimeStatus } from "../plugins/codex/scripts/lib/codex.mjs"; +import { BROKER_DISABLE_ENV } from "../plugins/codex/scripts/lib/app-server.mjs"; +import { makeTempDir } from "./helpers.mjs"; + +test("getSessionRuntimeStatus reports direct mode when disable-broker env is set", () => { + const cwd = makeTempDir(); + const env = { [BROKER_DISABLE_ENV]: "true" }; + + const status = getSessionRuntimeStatus(env, cwd); + + assert.equal(status.mode, "direct"); + assert.equal(status.endpoint, null); + assert.ok(status.detail.includes("disabled by CODEX_COMPANION_APP_SERVER_DISABLE_BROKER")); +}); + +test("getSessionRuntimeStatus honors disable-broker env even when endpoint is set", () => { + const cwd = makeTempDir(); + const env = { + [BROKER_DISABLE_ENV]: "1", + CODEX_COMPANION_APP_SERVER_ENDPOINT: "tcp://127.0.0.1:12345" + }; + + const status = getSessionRuntimeStatus(env, cwd); + + assert.equal(status.mode, "direct"); + assert.equal(status.endpoint, null); +}); + +test("getSessionRuntimeStatus reports shared mode when endpoint is set and disable is unset", () => { + const cwd = makeTempDir(); + const env = { + CODEX_COMPANION_APP_SERVER_ENDPOINT: "tcp://127.0.0.1:12345" + }; + + const status = getSessionRuntimeStatus(env, cwd); + + assert.equal(status.mode, "shared"); + assert.equal(status.endpoint, "tcp://127.0.0.1:12345"); +}); + +test("getSessionRuntimeStatus reports direct mode when no endpoint or disable flag is set", () => { + const cwd = makeTempDir(); + const status = getSessionRuntimeStatus({}, cwd); + + assert.equal(status.mode, "direct"); + assert.equal(status.endpoint, null); +});