diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 31c56646c4f..672645c3d5b 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -37,6 +37,7 @@ import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderAdapterValidationError } from "../Errors.ts"; import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; +import { sessionPortEnv } from "../sessionPort.ts"; import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts"; import type { ClaudeRtkRewriteRunner } from "./ClaudeRtkToolRewrite.ts"; const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); @@ -411,6 +412,24 @@ describe("ClaudeAdapterLive", () => { ); }); + it.effect("injects GITS_PORT into Claude SDK query env", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; + yield* adapter.startSession({ + threadId: THREAD_ID, + provider: ProviderDriverKind.make("claudeAgent"), + runtimeMode: "full-access", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.env?.GITS_PORT, sessionPortEnv(THREAD_ID).GITS_PORT); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + it.effect("maps the Claude Opus 4.7 default effort to the SDK-supported max value", () => { const harness = makeHarness(); return Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index bd92fc404ff..74bb11b833c 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -94,6 +94,7 @@ import { import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; import { type GitShimManagerShape } from "../GitShimManager.ts"; +import { sessionPortEnv } from "../sessionPort.ts"; const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.UnknownFromJsonString); @@ -1055,11 +1056,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( options: input.options, }) as ClaudeQueryRuntime); - // Git shim manager injected at construction; per-session shim env stored here. - // CONFINEMENT LINE: sessionShimEnvs are merged into queryOptions.env only — + // Git shim manager injected at construction; per-session child env stored here. + // CONFINEMENT LINE: sessionChildEnvs are merged into queryOptions.env only — // claudeEnvironment (instance-level) and server process.env are never mutated. const gitShimManager: GitShimManagerShape | undefined = options?.gitShimManager; - const sessionShimEnvs = new Map>(); + const sessionChildEnvs = new Map>(); const sessions = new Map(); const runtimeEventQueue = yield* Queue.bounded( @@ -2571,7 +2572,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( } sessions.delete(context.session.threadId); - sessionShimEnvs.delete(context.session.threadId); + sessionChildEnvs.delete(context.session.threadId); // Clean up the per-session shim dir when the session stops. if (gitShimManager) yield* gitShimManager.release(context.session.threadId); }); @@ -2999,6 +3000,27 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( ...(fastMode ? { fastMode: true } : {}), ...(ultracode ? { ultracode: true } : {}), }; + // Allocate per-session child env before creating the Claude SDK query; + // createQuery is the child-process spawn boundary for Claude. + // CONFINEMENT LINE: child vars merge into a claudeEnvironment copy only. + // Cleanup happens in stopSessionInternal via gitShimManager.release(). + // ponytail: shim denied/warn events log via Codex adapter's stderr pipeline; + // Claude SDK stderr is not captured here — add stderr capture when needed. + const childEnv = sessionPortEnv(threadId); + const sessionChildEnv = gitShimManager + ? { + ...childEnv, + ...(yield* gitShimManager + .allocate(threadId, input.cwd ?? "") + .pipe( + Effect.mapError((cause) => + toProcessError(cause, "Failed to allocate git confinement shim.", threadId), + ), + )).vars, + } + : childEnv; + sessionChildEnvs.set(threadId, sessionChildEnv); + const queryOptions: ClaudeQueryOptions = { ...(input.cwd ? { cwd: input.cwd } : {}), ...(apiModelId ? { model: apiModelId } : {}), @@ -3026,10 +3048,10 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( includePartialMessages: true, canUseTool, ...(visualPlanMcpServers ? { mcpServers: visualPlanMcpServers } : {}), - // Merge per-session shim env vars (GITS_ALLOWED_ROOT, PATH prepend, etc.) - // into the claude env. claudeEnvironment is the instance-level base; shimVars + // Merge per-session env vars (GITS_PORT, GITS_ALLOWED_ROOT, PATH prepend, etc.) + // into the claude env. claudeEnvironment is the instance-level base; child vars // add the per-session policy. Neither object is mutated. - env: Object.assign({}, claudeEnvironment, sessionShimEnvs.get(threadId) ?? {}), + env: Object.assign({}, claudeEnvironment, sessionChildEnv), ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}), ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}), }; @@ -3078,7 +3100,14 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( detail: toMessage(cause, "Failed to start Claude runtime session."), cause, }), - }); + }).pipe( + Effect.tapError(() => + Effect.gen(function* () { + sessionChildEnvs.delete(threadId); + if (gitShimManager) yield* gitShimManager.release(threadId); + }), + ), + ); const session: ProviderSession = { threadId, @@ -3123,23 +3152,6 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( yield* Ref.set(contextRef, context); sessions.set(threadId, context); - // Allocate git confinement shim for this session (non-supervisor sessions only). - // CONFINEMENT LINE: shimVars merged into claudeEnvironment copy per sendTurn only — - // claudeEnvironment (instance-level) and server process.env are never mutated. - // Cleanup happens in stopSessionInternal via gitShimManager.release(). - // ponytail: shim denied/warn events log via Codex adapter's stderr pipeline; - // Claude SDK stderr is not captured here — add stderr capture when needed. - if (gitShimManager) { - const shimResult = yield* gitShimManager - .allocate(threadId, input.cwd ?? "") - .pipe( - Effect.mapError((cause) => - toProcessError(cause, "Failed to allocate git confinement shim.", threadId), - ), - ); - sessionShimEnvs.set(threadId, shimResult.vars); - } - const sessionStartedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "session.started", diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index 4e228e2a130..89c759e87d9 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -39,6 +39,7 @@ import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderAdapterValidationError } from "../Errors.ts"; import type { CodexAdapterShape } from "../Services/CodexAdapter.ts"; import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; +import { sessionPortEnv } from "../sessionPort.ts"; import { type CodexSessionRuntimeOptions, type CodexForkResumeCursor, @@ -292,6 +293,7 @@ validationLayer("CodexAdapterLive validation", (it) => { assert.deepStrictEqual(validationRuntimeFactory.factory.mock.calls[0]?.[0], { binaryPath: "codex", cwd: process.cwd(), + environment: sessionPortEnv("thread-1"), model: "gpt-5.3-codex", providerInstanceId: ProviderInstanceId.make("codex"), serviceTier: "fast", @@ -407,6 +409,23 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { }), ); + it.effect("injects GITS_PORT into runtime environment", () => + Effect.gen(function* () { + const adapter = yield* CodexAdapter; + const threadId = asThreadId("sess-gits-port-codex"); + + yield* adapter.startSession({ + provider: ProviderDriverKind.make("codex"), + threadId, + runtimeMode: "full-access", + }); + + const runtime = sessionRuntimeFactory.lastRuntime; + assert.ok(runtime); + assert.equal(runtime.options.environment?.GITS_PORT, sessionPortEnv(threadId).GITS_PORT); + }), + ); + it.effect("maps codex model options for the adapter's bound custom instance id", () => { const customInstanceId = ProviderInstanceId.make("codex_personal"); const customRuntimeFactory = makeRuntimeFactory(); diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 07346eb9035..735741836f0 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -67,6 +67,7 @@ import { } from "./CodexSessionRuntime.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; import { type GitShimManagerShape } from "../GitShimManager.ts"; +import { sessionPortEnv } from "../sessionPort.ts"; const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError); const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError); const isCodexSessionRuntimeThreadIdMissingError = Schema.is( @@ -1440,10 +1441,12 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( if (options?.gitShimManager && Object.keys(shimEnv).length > 0) { yield* Scope.addFinalizer(sessionScope, options.gitShimManager.release(input.threadId)); } - const sessionEnv: NodeJS.ProcessEnv | undefined = - options?.environment != null || Object.keys(shimEnv).length > 0 - ? { ...(options?.environment ?? {}), ...shimEnv } - : undefined; + const sessionEnv: NodeJS.ProcessEnv = Object.assign( + {}, + options?.environment, + sessionPortEnv(input.threadId), + shimEnv, + ); const runtimeInput: CodexSessionRuntimeOptions = { threadId: input.threadId, @@ -1451,7 +1454,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( cwd: sessionCwd, binaryPath: codexConfig.binaryPath, visualPlanMcpUrl: `http://127.0.0.1:${serverConfig.port}${VISUAL_PLAN_MCP_PATH}`, - ...(sessionEnv != null ? { environment: sessionEnv } : {}), + environment: sessionEnv, ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}), ...(isCodexResumeCursorSchema(input.resumeCursor) ? { resumeCursor: input.resumeCursor } diff --git a/apps/server/src/provider/Layers/CursorAdapter.test.ts b/apps/server/src/provider/Layers/CursorAdapter.test.ts index 0c54c92b6dd..73256f51890 100644 --- a/apps/server/src/provider/Layers/CursorAdapter.test.ts +++ b/apps/server/src/provider/Layers/CursorAdapter.test.ts @@ -27,6 +27,7 @@ import { import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import type { CursorAdapterShape } from "../Services/CursorAdapter.ts"; +import { sessionPortEnv } from "../sessionPort.ts"; import { makeCursorAdapter } from "./CursorAdapter.ts"; const decodeCursorSettings = Schema.decodeSync(CursorSettings); @@ -80,6 +81,18 @@ exec ${JSON.stringify(bunExe)} ${JSON.stringify(mockAgentPath)} "$@" return wrapperPath; } +async function makeEnvProbeWrapper(envLogPath: string) { + const dir = await mkdtemp(path.join(os.tmpdir(), "cursor-acp-env-probe-")); + const wrapperPath = path.join(dir, "fake-agent.sh"); + const script = `#!/bin/sh +printf '%s\n' "\${GITS_PORT:-}" > ${JSON.stringify(envLogPath)} +exec ${JSON.stringify(bunExe)} ${JSON.stringify(mockAgentPath)} "$@" +`; + await writeFile(wrapperPath, script, "utf8"); + await chmod(wrapperPath, 0o755); + return wrapperPath; +} + async function readArgvLog(filePath: string) { const raw = await readFile(filePath, "utf8"); return raw @@ -231,6 +244,32 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { }), ); + it.effect("injects GITS_PORT into spawned ACP process env", () => + Effect.gen(function* () { + const adapter = yield* CursorAdapter; + const settings = yield* ServerSettingsService; + const threadId = ThreadId.make("cursor-gits-port-thread"); + const tempDir = yield* Effect.promise(() => + mkdtemp(path.join(os.tmpdir(), "cursor-gits-port-")), + ); + const envLogPath = path.join(tempDir, "env.log"); + + const wrapperPath = yield* Effect.promise(() => makeEnvProbeWrapper(envLogPath)); + yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); + + yield* adapter.startSession({ + threadId, + provider: ProviderDriverKind.make("cursor"), + cwd: process.cwd(), + runtimeMode: "full-access", + }); + + const raw = yield* Effect.promise(() => waitForFileContent(envLogPath)); + assert.equal(raw.trim(), sessionPortEnv(threadId).GITS_PORT); + yield* adapter.stopSession(threadId); + }), + ); + it.effect("closes the ACP child process when a session stops", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; diff --git a/apps/server/src/provider/Layers/CursorAdapter.ts b/apps/server/src/provider/Layers/CursorAdapter.ts index ce57adebf77..05c81db688d 100644 --- a/apps/server/src/provider/Layers/CursorAdapter.ts +++ b/apps/server/src/provider/Layers/CursorAdapter.ts @@ -81,6 +81,7 @@ import { type CursorAdapterShape } from "../Services/CursorAdapter.ts"; import { resolveCursorAcpBaseModelId } from "./CursorProvider.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; import { type GitShimManagerShape } from "../GitShimManager.ts"; +import { sessionPortEnv } from "../sessionPort.ts"; const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); const PROVIDER = ProviderDriverKind.make("cursor"); @@ -559,14 +560,16 @@ export function makeCursorAdapter( if (options?.gitShimManager && Object.keys(cursorShimEnv).length > 0) { yield* Scope.addFinalizer(sessionScope, options.gitShimManager.release(input.threadId)); } - const cursorSessionEnv: NodeJS.ProcessEnv | undefined = - options?.environment != null || Object.keys(cursorShimEnv).length > 0 - ? { ...(options?.environment ?? {}), ...cursorShimEnv } - : undefined; + const cursorSessionEnv: NodeJS.ProcessEnv = Object.assign( + {}, + options?.environment, + sessionPortEnv(input.threadId), + cursorShimEnv, + ); const acp = yield* makeCursorAcpRuntime({ cursorSettings: effectiveCursorSettings, - ...(cursorSessionEnv != null ? { environment: cursorSessionEnv } : {}), + environment: cursorSessionEnv, childProcessSpawner, cwd, ...(resumeSessionId ? { resumeSessionId } : {}), diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts index 4849272035c..fec819d14b6 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts @@ -24,6 +24,7 @@ import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; +import { sessionPortEnv } from "../sessionPort.ts"; import { OpenCodeRuntime, OpenCodeRuntimeError, @@ -55,6 +56,7 @@ const runtimeMock = { startCalls: [] as string[], sessionCreateUrls: [] as string[], authHeaders: [] as Array, + connectEnvironments: [] as Array, abortCalls: [] as string[], closeCalls: [] as string[], revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, @@ -68,6 +70,7 @@ const runtimeMock = { this.state.startCalls.length = 0; this.state.sessionCreateUrls.length = 0; this.state.authHeaders.length = 0; + this.state.connectEnvironments.length = 0; this.state.abortCalls.length = 0; this.state.closeCalls.length = 0; this.state.revertCalls.length = 0; @@ -97,8 +100,9 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { exitCode: Effect.never, }; }), - connectToOpenCodeServer: ({ serverUrl }) => + connectToOpenCodeServer: ({ serverUrl, environment }) => Effect.gen(function* () { + runtimeMock.state.connectEnvironments.push(environment); const url = serverUrl ?? "http://127.0.0.1:4301"; // Unconditionally register a scope finalizer for test observability — // preserves the `closeCalls` / `closeError` probes that the existing @@ -245,6 +249,10 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { assert.deepEqual(runtimeMock.state.authHeaders, [ `Basic ${btoa("opencode:secret-password")}`, ]); + assert.equal( + runtimeMock.state.connectEnvironments[0]?.GITS_PORT, + sessionPortEnv("thread-opencode").GITS_PORT, + ); }), ); diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.ts index 84cd9b0c57e..024a5e58d66 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.ts @@ -28,6 +28,7 @@ import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; import { type GitShimManagerShape } from "../GitShimManager.ts"; +import { sessionPortEnv } from "../sessionPort.ts"; import { ProviderAdapterProcessError, ProviderAdapterRequestError, @@ -1052,10 +1053,12 @@ export function makeOpenCodeAdapter( .allocate(input.threadId, directory) .pipe(Effect.mapError((cause) => toProcessError(input.threadId, cause)))).vars : {}; - const openCodeSessionEnv: NodeJS.ProcessEnv | undefined = - options?.environment != null || Object.keys(openCodeShimEnv).length > 0 - ? { ...(options?.environment ?? {}), ...openCodeShimEnv } - : undefined; + const openCodeSessionEnv: NodeJS.ProcessEnv = Object.assign( + {}, + options?.environment, + sessionPortEnv(input.threadId), + openCodeShimEnv, + ); const started = yield* Effect.gen(function* () { const sessionScope = yield* Scope.make(); @@ -1070,7 +1073,7 @@ export function makeOpenCodeAdapter( const server = yield* openCodeRuntime.connectToOpenCodeServer({ binaryPath, serverUrl, - ...(openCodeSessionEnv != null ? { environment: openCodeSessionEnv } : {}), + environment: openCodeSessionEnv, }); const client = openCodeRuntime.createOpenCodeSdkClient({ baseUrl: server.url, diff --git a/apps/server/src/provider/sessionPort.test.ts b/apps/server/src/provider/sessionPort.test.ts new file mode 100644 index 00000000000..75f5b4e83f0 --- /dev/null +++ b/apps/server/src/provider/sessionPort.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { sessionPortEnv, sessionPortForSessionId } from "./sessionPort.ts"; + +describe("sessionPortForSessionId", () => { + it("returns a stable high port for the same session id", () => { + const first = sessionPortForSessionId("thread-session-a"); + const second = sessionPortForSessionId("thread-session-a"); + + expect(first).toBe(second); + expect(first).toBeGreaterThanOrEqual(30_000); + expect(first).toBeLessThan(40_000); + }); + + it("spreads typical session ids across different ports", () => { + const ports = Array.from({ length: 128 }, (_, index) => + sessionPortForSessionId(`thread-${index}`), + ); + + expect(new Set(ports).size).toBe(ports.length); + }); + + it("serializes the port as GITS_PORT env", () => { + expect(sessionPortEnv("thread-session-a")).toEqual({ + GITS_PORT: String(sessionPortForSessionId("thread-session-a")), + }); + }); +}); diff --git a/apps/server/src/provider/sessionPort.ts b/apps/server/src/provider/sessionPort.ts new file mode 100644 index 00000000000..ef49c65d0f2 --- /dev/null +++ b/apps/server/src/provider/sessionPort.ts @@ -0,0 +1,17 @@ +const SESSION_PORT_BASE = 30_000; +const SESSION_PORT_SPAN = 10_000; + +export function sessionPortForSessionId(sessionId: string): number { + let hash = 0x811c9dc5; + for (let index = 0; index < sessionId.length; index += 1) { + hash ^= sessionId.charCodeAt(index); + hash = Math.imul(hash, 0x01000193); + } + // ponytail: deterministic hash only; replace with a real allocator if active + // sessions ever collide in the 30000-39999 dev-server range. + return SESSION_PORT_BASE + ((hash >>> 0) % SESSION_PORT_SPAN); +} + +export function sessionPortEnv(sessionId: string): Record { + return { GITS_PORT: String(sessionPortForSessionId(sessionId)) }; +} diff --git a/apps/server/src/terminal/Layers/Manager.test.ts b/apps/server/src/terminal/Layers/Manager.test.ts index c7a321d1195..4b3ae5bb0f4 100644 --- a/apps/server/src/terminal/Layers/Manager.test.ts +++ b/apps/server/src/terminal/Layers/Manager.test.ts @@ -26,6 +26,7 @@ import { TestClock } from "effect/testing"; import { expect } from "vitest"; import * as ProcessRunner from "../../processRunner.ts"; +import { sessionPortEnv } from "../../provider/sessionPort.ts"; import type { TerminalManagerShape } from "../Services/Manager.ts"; import { type PtyAdapterShape, @@ -1229,6 +1230,23 @@ it.layer( }), ); + it.effect("injects deterministic GITS_PORT into spawned terminals", () => + Effect.gen(function* () { + const { manager, ptyAdapter } = yield* createManager(); + yield* manager.open( + openInput({ + threadId: "thread-terminal-port", + env: { GITS_PORT: "9999" }, + }), + ); + const spawnInput = ptyAdapter.spawnInputs[0]; + expect(spawnInput).toBeDefined(); + if (!spawnInput) return; + + assert.equal(spawnInput.env.GITS_PORT, sessionPortEnv("thread-terminal-port").GITS_PORT); + }), + ); + it.effect("starts zsh with prompt spacer disabled to avoid `%` end markers", () => Effect.gen(function* () { if (process.platform === "win32") return; diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index 82eb9502cf6..53df280ef00 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -48,6 +48,7 @@ import { type PtyExitEvent, type PtyProcess, } from "../Services/PTY.ts"; +import { sessionPortEnv } from "../../provider/sessionPort.ts"; const DEFAULT_HISTORY_LINE_LIMIT = 5_000; const DEFAULT_PERSIST_DEBOUNCE_MS = 40; @@ -1632,7 +1633,10 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith Effect.andThen( Effect.gen(function* () { const shellCandidates = resolveShellCandidates(shellResolver, platform, baseEnv); - const terminalEnv = createTerminalSpawnEnv(baseEnv, session.runtimeEnv); + const portEnv = sessionPortEnv(session.threadId); + const terminalRuntimeEnv = + session.runtimeEnv === null ? portEnv : { ...session.runtimeEnv, ...portEnv }; + const terminalEnv = createTerminalSpawnEnv(baseEnv, terminalRuntimeEnv); const spawnResult = yield* trySpawn(shellCandidates, terminalEnv, session); ptyProcess = spawnResult.process; startedShell = spawnResult.shellLabel;