Browse Source

refactor(cli): manage environment variables with effect config

Dax Raad 1 month ago
parent
commit
24d26365e6

+ 6 - 3
packages/cli/src/commands/handlers/default.ts

@@ -1,8 +1,9 @@
 import { NodeFileSystem } from "@effect/platform-node"
 import { Commands } from "../commands"
 import { Runtime } from "../../framework/runtime"
-import { Effect, Option } from "effect"
+import { Effect, Option, Redacted } from "effect"
 import { Service } from "@opencode-ai/client/effect"
+import { Env } from "../../env"
 import { ServiceConfig } from "../../services/service-config"
 import { Standalone } from "../../services/standalone"
 import { Updater } from "../../services/updater"
@@ -18,10 +19,12 @@ export default Runtime.handler(Commands, (input) =>
       return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
     const transport = yield* Effect.gen(function* () {
       if (server !== undefined) {
-        const password = process.env["OPENCODE_PASSWORD"]
+        const password = Option.getOrUndefined(yield* Env.password)
         const explicit = {
           url: server,
-          headers: password ? { authorization: "Basic " + btoa("opencode:" + password) } : undefined,
+          headers: password
+            ? { authorization: "Basic " + btoa("opencode:" + Redacted.value(password)) }
+            : undefined,
         } satisfies Service.Transport
         // Fail loudly before entering the TUI: an explicit server that is
         // unreachable or rejects auth should not present as reconnect churn.

+ 8 - 3
packages/cli/src/commands/handlers/serve.ts

@@ -4,7 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { PermissionSaved } from "@opencode-ai/core/permission/saved"
 import { Global } from "@opencode-ai/core/global"
-import { Context, FileSystem, Layer, Option, Schedule, Schema } from "effect"
+import { Context, FileSystem, Layer, Option, Redacted, Schedule, Schema } from "effect"
 import * as Effect from "effect/Effect"
 import { HttpRouter, HttpServer } from "effect/unstable/http"
 import { createServer } from "node:http"
@@ -15,6 +15,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
 import { Commands } from "../commands"
 import { Runtime } from "../../framework/runtime"
 import { Service } from "@opencode-ai/client/effect"
+import { Env } from "../../env"
 import { ServiceConfig } from "../../services/service-config"
 import { Updater } from "../../services/updater"
 import { randomBytes, randomUUID } from "crypto"
@@ -26,12 +27,16 @@ export default Runtime.handler(
     if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
     return yield* Effect.scoped(
       Effect.gen(function* () {
-        const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD
+        const standalonePassword = Option.getOrUndefined(yield* Env.serverPassword)
+        // Keep the lease credential out of the environment inherited by any
+        // process this server spawns.
         if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
         const config = input.service ? yield* ServiceConfig.read() : {}
         const password = input.service
           ? yield* ServiceConfig.password()
-          : standalonePassword || randomBytes(32).toString("base64url")
+          : standalonePassword
+            ? Redacted.value(standalonePassword)
+            : randomBytes(32).toString("base64url")
         if (!password) return yield* Effect.fail(new Error("Missing server password"))
         const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
         const port = Option.isSome(input.port)

+ 18 - 0
packages/cli/src/env.ts

@@ -0,0 +1,18 @@
+import { Config } from "effect"
+
+// Every environment variable the CLI reads, in one place. Consumers yield
+// these instead of touching process.env so the full surface stays visible,
+// typed, and redacted where secret.
+
+// Client-side password for an explicit --server target. The legacy name is
+// still honored; it also remains the variable a standalone child inherits.
+export const password = Config.redacted("OPENCODE_PASSWORD").pipe(
+  Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")),
+  Config.option,
+)
+
+// Server-side lease password: set by the standalone spawner for its child,
+// or preset for a manually managed `opencode serve`.
+export const serverPassword = Config.redacted("OPENCODE_SERVER_PASSWORD").pipe(Config.option)
+
+export * as Env from "./env"