| author | |
| committer | |
| log | 4b4abcdd7d197647b8b21b18d2344e4e12ae4c34 |
| tree | 52aa27db906ff72f2cf5dcf0f2816e0001393415 |
| parent | eb35685aeb7b891c9e0a33a1d5061c335204852b |
- 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 keys2 files changed, 73 insertions(+), 1 deletions(-)
apps/server/src/terminalManager.test.ts+45| ... | ... | @@ -424,4 +424,49 @@ describe("TerminalManager", () => { |
| 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 | }); |
apps/server/src/terminalManager.ts+28-1| ... | ... | @@ -26,6 +26,11 @@ import { runProcess } from "./processRunner"; |
| 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 { |
| 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> { |
| 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> { |
| 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; |