- build terminal spawn env from a filtered copy of process env - exclude `PORT`, `T3CODE_*`, `VITE_*`, and Electron runtime vars - add test coverage to verify filtered and preserved env keys
Unified · Split
| @@ -424,4 +424,49 @@ describe("TerminalManager", () => { · 423 unmodified lines | ||
| 424 | 424 | |
| 425 | 425 | manager.dispose(); |
| 426 | 426 | }); |
| 427 | + | |
| 428 | + it("filters app runtime env variables from terminal sessions", async () => { | |
| 429 | + const originalValues = new Map<string, string | undefined>(); | |
| 430 | + const setEnv = (key: string, value: string | undefined) => { | |
| 431 | + if (!originalValues.has(key)) { | |
| 432 | + originalValues.set(key, process.env[key]); | |
| 433 | + } | |
| 434 | + if (value === undefined) { | |
| 435 | + delete process.env[key]; | |
| 436 | + return; | |
| 437 | + } | |
| 438 | + process.env[key] = value; | |
| 439 | + }; | |
| 440 | + const restoreEnv = () => { | |
| 441 | + for (const [key, value] of originalValues) { | |
| 442 | + if (value === undefined) { | |
| 443 | + delete process.env[key]; | |
| 444 | + } else { | |
| 445 | + process.env[key] = value; | |
| 446 | + } | |
| 447 | + } | |
| 448 | + }; | |
| 449 | + | |
| 450 | + setEnv("PORT", "5173"); | |
| 451 | + setEnv("T3CODE_PORT", "3773"); | |
| 452 | + setEnv("VITE_DEV_SERVER_URL", "http://localhost:5173"); | |
| 453 | + setEnv("TEST_TERMINAL_KEEP", "keep-me"); | |
| 454 | + | |
| 455 | + try { | |
| 456 | + const { manager, ptyAdapter } = makeManager(); | |
| 457 | + await manager.open(openInput()); | |
| 458 | + const spawnInput = ptyAdapter.spawnInputs[0]; | |
| 459 | + expect(spawnInput).toBeDefined(); | |
| 460 | + if (!spawnInput) return; | |
| 461 | + | |
| 462 | + expect(spawnInput.env.PORT).toBeUndefined(); | |
| 463 | + expect(spawnInput.env.T3CODE_PORT).toBeUndefined(); | |
| 464 | + expect(spawnInput.env.VITE_DEV_SERVER_URL).toBeUndefined(); | |
| 465 | + expect(spawnInput.env.TEST_TERMINAL_KEEP).toBe("keep-me"); | |
| 466 | + | |
| 467 | + manager.dispose(); | |
| 468 | + } finally { | |
| 469 | + restoreEnv(); | |
| 470 | + } | |
| 471 | + }); | |
| 427 | 472 | }); |
| @@ -26,6 +26,11 @@ import { runProcess } from "./processRunner"; · 25 unmodified lines | ||
| 26 | 26 | const DEFAULT_HISTORY_LINE_LIMIT = 5_000; |
| 27 | 27 | const DEFAULT_PERSIST_DEBOUNCE_MS = 40; |
| 28 | 28 | const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000; |
| 29 | +const TERMINAL_ENV_BLOCKLIST = new Set([ | |
| 30 | + "PORT", | |
| 31 | + "ELECTRON_RENDERER_PORT", | |
| 32 | + "ELECTRON_RUN_AS_NODE", | |
| 33 | +]); | |
| 29 | 34 | |
| 30 | 35 | type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>; |
| 31 | 36 | |
| @@ -234,6 +239,27 @@ function toSessionKey(threadId: string, terminalId: string): string { · 202 unmodified lines | ||
| 234 | 239 | return `${threadId}\u0000${terminalId}`; |
| 235 | 240 | } |
| 236 | 241 | |
| 242 | +function shouldExcludeTerminalEnvKey(key: string): boolean { | |
| 243 | + const normalizedKey = key.toUpperCase(); | |
| 244 | + if (normalizedKey.startsWith("T3CODE_")) { | |
| 245 | + return true; | |
| 246 | + } | |
| 247 | + if (normalizedKey.startsWith("VITE_")) { | |
| 248 | + return true; | |
| 249 | + } | |
| 250 | + return TERMINAL_ENV_BLOCKLIST.has(normalizedKey); | |
| 251 | +} | |
| 252 | + | |
| 253 | +function createTerminalSpawnEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { | |
| 254 | + const spawnEnv: NodeJS.ProcessEnv = {}; | |
| 255 | + for (const [key, value] of Object.entries(baseEnv)) { | |
| 256 | + if (value === undefined) continue; | |
| 257 | + if (shouldExcludeTerminalEnvKey(key)) continue; | |
| 258 | + spawnEnv[key] = value; | |
| 259 | + } | |
| 260 | + return spawnEnv; | |
| 261 | +} | |
| 262 | + | |
| 237 | 263 | export class TerminalManager extends EventEmitter<TerminalManagerEvents> { |
| 238 | 264 | private readonly sessions = new Map<string, TerminalSessionState>(); |
| 239 | 265 | private readonly logsDir: string; |
| @@ -467,6 +493,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 227 unmodified lines | ||
| 467 | 493 | let startedShell: string | null = null; |
| 468 | 494 | try { |
| 469 | 495 | const shellCandidates = resolveShellCandidates(this.shellResolver); |
| 496 | + const terminalEnv = createTerminalSpawnEnv(process.env); | |
| 470 | 497 | let lastSpawnError: unknown = null; |
| 471 | 498 | |
| 472 | 499 | for (const shell of shellCandidates) { |
| @@ -476,7 +503,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 3 unmodified lines | ||
| 476 | 503 | cwd: session.cwd, |
| 477 | 504 | cols: session.cols, |
| 478 | 505 | rows: session.rows, |
| 479 | − env: process.env, | |
| 506 | + env: terminalEnv, | |
| 480 | 507 | }); |
| 481 | 508 | startedShell = shell; |
| 482 | 509 | break; |