From 7287a90aec844af3ddd40039c43971dda21f2a6f Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sat, 4 Apr 2026 13:49:33 -0500 Subject: [PATCH] Propagate project runtime env to git and provider actions - Resolve runtime env from project context and cwd - Provide it to git, gh, and Codex-backed processes --- .../src/git/Layers/CodexTextGeneration.ts | 7 +- apps/server/src/git/Layers/GitCore.ts | 18 ++-- apps/server/src/git/Layers/GitHubCli.ts | 25 ++++-- .../Layers/ProviderCommandReactor.ts | 2 + apps/server/src/runtimeEnvironment.ts | 24 ++++- apps/server/src/wsServer.ts | 87 ++++++++++++++----- 6 files changed, 126 insertions(+), 37 deletions(-) diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index c14e4dd31..a17d07180 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -2,12 +2,14 @@ import { randomUUID } from "node:crypto"; import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; +import { compactNodeProcessEnv, mergeNodeProcessEnv } from "@okcode/shared/environment"; import { DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@okcode/contracts"; import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@okcode/shared/git"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; +import { getRuntimeEnv } from "../../runtimeEnvironment.ts"; import { TextGenerationError } from "../Errors.ts"; import { type BranchNameGenerationInput, @@ -227,7 +229,10 @@ const makeCodexTextGeneration = Effect.gen(function* () { { cwd, shell: process.platform === "win32", - env: process.env, + env: mergeNodeProcessEnv( + compactNodeProcessEnv(process.env), + compactNodeProcessEnv(yield* getRuntimeEnv()), + ), stdin: { stream: Stream.make(new TextEncoder().encode(prompt)), }, diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 3552907e3..e1d721447 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -21,6 +21,8 @@ import { import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { compactNodeProcessEnv, mergeNodeProcessEnv } from "@okcode/shared/environment"; +import { getRuntimeEnv } from "../../runtimeEnvironment.ts"; + import { GitCommandError } from "../Errors.ts"; import { GitCore, @@ -592,17 +594,19 @@ export const makeGitCore = (options?: { executeOverride?: GitCoreShape["execute" Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.mapError(toGitCommandError(commandInput, "failed to create trace2 monitor.")), ); - const runtimeEnv = input.env ? compactNodeProcessEnv(input.env) : undefined; + const contextEnv = yield* getRuntimeEnv(); + const explicitEnv = input.env ? compactNodeProcessEnv(input.env) : undefined; + const baseEnv = compactNodeProcessEnv(process.env); + const combinedEnv = { + ...baseEnv, + ...contextEnv, + ...explicitEnv, + }; const child = yield* commandSpawner .spawn( ChildProcess.make("git", commandInput.args, { cwd: commandInput.cwd, - env: mergeNodeProcessEnv( - compactNodeProcessEnv( - mergeNodeProcessEnv(compactNodeProcessEnv(process.env), runtimeEnv), - ), - compactNodeProcessEnv(trace2Monitor.env), - ), + env: mergeNodeProcessEnv(combinedEnv, compactNodeProcessEnv(trace2Monitor.env)), }), ) .pipe(Effect.mapError(toGitCommandError(commandInput, "failed to spawn."))); diff --git a/apps/server/src/git/Layers/GitHubCli.ts b/apps/server/src/git/Layers/GitHubCli.ts index f616c4470..a82d8ea51 100644 --- a/apps/server/src/git/Layers/GitHubCli.ts +++ b/apps/server/src/git/Layers/GitHubCli.ts @@ -1,7 +1,9 @@ import { Effect, Layer, Schema } from "effect"; import { PositiveInt, TrimmedNonEmptyString } from "@okcode/contracts"; +import { compactNodeProcessEnv, mergeNodeProcessEnv } from "@okcode/shared/environment"; import { runProcess } from "../../processRunner"; +import { getRuntimeEnv } from "../../runtimeEnvironment.ts"; import { GitHubCliError } from "../Errors.ts"; import { GitHubCli, @@ -167,14 +169,21 @@ function decodeGitHubJson( const makeGitHubCli = Effect.sync(() => { const execute: GitHubCliShape["execute"] = (input) => - Effect.tryPromise({ - try: () => - runProcess("gh", input.args, { - cwd: input.cwd, - timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, - env: process.env, - }), - catch: (error) => normalizeGitHubCliError("execute", error), + Effect.gen(function* () { + const contextEnv = yield* getRuntimeEnv(); + const mergedEnv = mergeNodeProcessEnv( + compactNodeProcessEnv(process.env), + compactNodeProcessEnv(contextEnv), + ); + return yield* Effect.tryPromise({ + try: () => + runProcess("gh", input.args, { + cwd: input.cwd, + timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, + env: mergedEnv, + }), + catch: (error) => normalizeGitHubCliError("execute", error), + }); }); const service = { diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index 6ec0b2c17..79c229258 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -337,6 +337,8 @@ const make = Effect.gen(function* () { ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), env: yield* resolveRuntimeEnvironment({ projectId: thread.projectId, + cwd: effectiveCwd ?? null, + readModel, }), runtimeMode: desiredRuntimeMode, }); diff --git a/apps/server/src/runtimeEnvironment.ts b/apps/server/src/runtimeEnvironment.ts index 7461781f0..6b87fe943 100644 --- a/apps/server/src/runtimeEnvironment.ts +++ b/apps/server/src/runtimeEnvironment.ts @@ -1,5 +1,5 @@ import type { OrchestrationReadModel, ProjectId } from "@okcode/contracts"; -import { Effect } from "effect"; +import { Effect, Option, ServiceMap } from "effect"; import { mergeEnvironmentRecords, @@ -9,6 +9,28 @@ import { import { EnvironmentVariables } from "./persistence/Services/EnvironmentVariables"; +/** + * Optional service carrying resolved runtime environment variables. + * + * Provided via `Effect.provideService` at the handler boundary so that + * downstream services (GitCore, GitHubCli, …) can read the project-/global- + * scoped env without requiring explicit parameter threading through every + * layer. + * + * Uses `Effect.serviceOption` on the consumer side so that the service + * requirement does NOT propagate into every downstream type signature. + */ +export class RuntimeEnv extends ServiceMap.Service()( + "okcode/RuntimeEnv", +) {} + +/** + * Read the current runtime environment from the fiber context. + * Returns an empty record when the service has not been provided. + */ +export const getRuntimeEnv = (): Effect.Effect => + Effect.serviceOption(RuntimeEnv).pipe(Effect.map((opt) => (Option.isSome(opt) ? opt.value : {}))); + export interface RuntimeEnvironmentInput { readonly projectId?: ProjectId | null; readonly cwd?: string | null; diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 367fc5d53..42126b52d 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -88,7 +88,7 @@ import { GitActionExecutionError } from "./git/Errors.ts"; import { EnvironmentVariables } from "./persistence/Services/EnvironmentVariables.ts"; import { SkillService } from "./skills/SkillService.ts"; import { TokenManager } from "./tokenManager.ts"; -import { resolveRuntimeEnvironment } from "./runtimeEnvironment.ts"; +import { resolveRuntimeEnvironment, RuntimeEnv } from "./runtimeEnvironment.ts"; import { version as serverVersion } from "../package.json" with { type: "json" }; /** @@ -1070,38 +1070,60 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< case WS_METHODS.gitStatus: { const body = stripRequestTag(request.body); - return yield* gitManager.status(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* gitManager.status(body).pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitPull: { const body = stripRequestTag(request.body); - return yield* git.pullCurrentBranch(body.cwd); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* git + .pullCurrentBranch(body.cwd) + .pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitRunStackedAction: { const body = stripRequestTag(request.body); - return yield* gitManager.runStackedAction(body, { - actionId: body.actionId, - progressReporter: { - publish: (event) => - pushBus.publishClient(ws, WS_CHANNELS.gitActionProgress, event).pipe(Effect.asVoid), - }, - }); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* gitManager + .runStackedAction(body, { + actionId: body.actionId, + progressReporter: { + publish: (event) => + pushBus.publishClient(ws, WS_CHANNELS.gitActionProgress, event).pipe(Effect.asVoid), + }, + }) + .pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitResolvePullRequest: { const body = stripRequestTag(request.body); - return yield* gitManager.resolvePullRequest(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* gitManager + .resolvePullRequest(body) + .pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitPreparePullRequestThread: { const body = stripRequestTag(request.body); - return yield* gitManager.preparePullRequestThread(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* gitManager + .preparePullRequestThread(body) + .pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitListPullRequests: { const body = stripRequestTag(request.body); - return yield* gitManager.listPullRequests(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* gitManager + .listPullRequests(body) + .pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.prReviewGetConfig: { @@ -1226,37 +1248,58 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< case WS_METHODS.gitListBranches: { const body = stripRequestTag(request.body); - return yield* git.listBranches(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* git.listBranches(body).pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitCreateWorktree: { const body = stripRequestTag(request.body); - return yield* git.createWorktree(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* git.createWorktree(body).pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitRemoveWorktree: { const body = stripRequestTag(request.body); - return yield* git.removeWorktree(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* git.removeWorktree(body).pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitCreateBranch: { const body = stripRequestTag(request.body); - return yield* git.createBranch(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* Effect.scoped(git.createBranch(body)).pipe( + Effect.provideService(RuntimeEnv, gitEnv), + ); } case WS_METHODS.gitCheckout: { const body = stripRequestTag(request.body); - return yield* Effect.scoped(git.checkoutBranch(body)); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* Effect.scoped(git.checkoutBranch(body)).pipe( + Effect.provideService(RuntimeEnv, gitEnv), + ); } case WS_METHODS.gitInit: { const body = stripRequestTag(request.body); - return yield* git.initRepo(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ cwd: body.cwd, readModel: snapshot }); + return yield* git.initRepo(body).pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.gitCloneRepository: { const body = stripRequestTag(request.body); - return yield* git.cloneRepository(body); + const snapshot = yield* projectionReadModelQuery.getSnapshot(); + const gitEnv = yield* resolveRuntimeEnvironment({ + cwd: body.targetDir, + readModel: snapshot, + }); + return yield* git.cloneRepository(body).pipe(Effect.provideService(RuntimeEnv, gitEnv)); } case WS_METHODS.terminalOpen: { @@ -1267,6 +1310,8 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< snapshot.threads.find( (thread) => thread.id === body.threadId && thread.deletedAt === null, )?.projectId ?? null, + cwd: body.cwd, + readModel: snapshot, ...(body.env !== undefined ? { extraEnv: body.env } : {}), }); return yield* terminalManager.open({ @@ -1298,6 +1343,8 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< snapshot.threads.find( (thread) => thread.id === body.threadId && thread.deletedAt === null, )?.projectId ?? null, + cwd: body.cwd, + readModel: snapshot, ...(body.env !== undefined ? { extraEnv: body.env } : {}), }); return yield* terminalManager.restart({