diff --git a/desktop/agent-setup.mjs b/desktop/agent-setup.mjs index ed572f5..3384af2 100644 --- a/desktop/agent-setup.mjs +++ b/desktop/agent-setup.mjs @@ -21,6 +21,17 @@ import { validateProjectId } from "./project-binding.mjs"; const GUIDE_NAME = "VIDEO_FS_AGENT_GUIDE.md"; +/** Codex starts an MCP server with a short fixed list of environment variables. + * On Linux the launcher is the desktop app, which exits at startup when it + * cannot reach the session's display, so Codex has to pass these on as well. */ +const CODEX_DISPLAY_ENV_VARS = [ + "DISPLAY", + "WAYLAND_DISPLAY", + "XAUTHORITY", + "XDG_RUNTIME_DIR", + "XDG_SESSION_TYPE", +]; + export function projectIdFromAppUrl(candidate, appUrl) { let url; try { @@ -70,6 +81,9 @@ export async function setupAgentProject({ projectId: rawProjectId, state }) { "[mcp_servers.video-fs]", `command = ${tomlString(launcher.command)}`, `args = ${tomlStringArray(launcher.args)}`, + ...(process.platform === "linux" + ? [`env_vars = ${tomlStringArray(CODEX_DISPLAY_ENV_VARS)}`] + : []), "", ].join("\n"); const claudeHookConfig = hookConfig(claudeHookLauncher); diff --git a/desktop/agent-setup.test.mjs b/desktop/agent-setup.test.mjs index 95bf0b8..f26c0b3 100644 --- a/desktop/agent-setup.test.mjs +++ b/desktop/agent-setup.test.mjs @@ -337,6 +337,15 @@ test("both generated launchers bind exactly the opened project", async () => { ); assert.match(codex, /args = \["--mcp", "--project-id", "bound-project"\]/); assert.doesNotMatch(codex, /project-a|project-b/); + // Codex hands an MCP server none of the display variables unless they are + // named, and on Linux the launcher cannot start without them. + assert.equal( + codex.includes( + 'env_vars = ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "XDG_SESSION_TYPE"]\n', + ), + process.platform === "linux", + ); + assert.equal(codex.includes("env_vars"), process.platform === "linux"); const claudeHooks = JSON.parse( await readFile(