Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
bf21d10
fix(env): proxy directly to process.env instead of snapshotting
jerome-benoit Apr 27, 2026
87ab94a
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 27, 2026
6b29710
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 27, 2026
6d9a331
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 27, 2026
f94cdf5
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 27, 2026
6c7c591
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 28, 2026
3deec72
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 28, 2026
69a6dbd
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 28, 2026
dbb73f4
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 28, 2026
d506d6a
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 29, 2026
7186b14
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 30, 2026
e6c72c9
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 30, 2026
0237fa9
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 30, 2026
7702475
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 30, 2026
84b46ff
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit Apr 30, 2026
45b50db
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 1, 2026
0ae35e0
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 1, 2026
873795f
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 2, 2026
414f6fb
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 2, 2026
f2efe23
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 2, 2026
301e91c
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 3, 2026
a8220d0
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 3, 2026
9e11919
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 3, 2026
6916a66
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 3, 2026
ecf064a
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 4, 2026
5364150
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 4, 2026
71700a9
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 4, 2026
5be5f9e
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 5, 2026
627fae5
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 5, 2026
8ba4d4a
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 5, 2026
dbf5377
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 6, 2026
2420cd2
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 6, 2026
a418e01
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 7, 2026
427b041
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 7, 2026
4d5e41d
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 7, 2026
221c78c
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 7, 2026
39a9a7d
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 7, 2026
2710515
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 7, 2026
721247e
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 7, 2026
543b691
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 7, 2026
702d101
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 8, 2026
899417b
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 8, 2026
fb70260
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 8, 2026
855b1db
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 8, 2026
e52cfd7
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 8, 2026
02378cf
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 8, 2026
b3629e9
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 8, 2026
4732470
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
af7d7c8
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
741ac48
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
24e066d
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
aa12b4b
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
a0eee54
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
33f5cc1
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
fa66f0a
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
3d4d2ea
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
26185ea
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 9, 2026
d7739ef
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 10, 2026
a8b3424
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 10, 2026
c46d1eb
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 10, 2026
7ff27e3
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 11, 2026
2606d3b
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 11, 2026
0aa089e
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 12, 2026
e84e544
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 12, 2026
3ba3236
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 12, 2026
f3c5b0e
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
c3a45b6
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
cbe500b
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
8d93db6
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
2b8901c
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
eba83f7
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
cab01a8
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
7df73af
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
e328eee
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
27fd04e
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
cd84dbf
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
20c5453
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 13, 2026
8989c59
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 14, 2026
9ab890d
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 14, 2026
5d41d24
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 14, 2026
cec558e
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 14, 2026
6315ffc
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 14, 2026
db64ec1
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 15, 2026
9b25e2b
test(preload): clear remaining provider-detection env vars
jerome-benoit May 15, 2026
49ba300
fix(provider): re-evaluate env detection in list()/getProvider() per …
jerome-benoit May 15, 2026
00d9d67
fix(provider): route all readers through env overlay, pre-clean database
jerome-benoit May 15, 2026
4f480b3
refactor(provider): share keepModel/prepareModel between cachedProvid…
jerome-benoit May 15, 2026
1a486bc
fix(provider): complete late-env detection contract — multi-cred opt-…
jerome-benoit May 15, 2026
2fa8b82
fix(provider): harden getLanguage against late-env removal/rotation
jerome-benoit May 15, 2026
383fce5
fix(provider): address review feedback on env overlay PR
jerome-benoit May 15, 2026
490df46
fix(provider): post-review polish — hashIdentity comment, hot-path tr…
jerome-benoit May 15, 2026
e699d09
fix(provider): post-review hardenings — vars live-env, whitespace, Bi…
jerome-benoit May 15, 2026
4a3bb6a
refactor(provider): trim slop comments
jerome-benoit May 15, 2026
d30bdf3
refactor(provider): lift keepModel/prepareModel to top-level helpers
jerome-benoit May 15, 2026
446a1df
refactor(provider): route vars() process.env reads through liveEnv() …
jerome-benoit May 15, 2026
4e6d8ef
refactor(provider): rename dynamicEnv:false to requiresRestart:true
jerome-benoit May 15, 2026
fce109c
refactor(provider): extract pure overlay helpers to provider/overlay.ts
jerome-benoit May 15, 2026
e22a3a7
test(provider): per-provider requires-restart regression + warn-once …
jerome-benoit May 15, 2026
3b2e926
docs(provider): document hashIdentity unsupported input types
jerome-benoit May 15, 2026
08bf75d
test(preload): drive env deletion list from models fixture
jerome-benoit May 15, 2026
226a792
perf(provider): bounded LRU + thundering-herd dedup; pass envs to vars()
jerome-benoit May 15, 2026
6251cdb
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 15, 2026
b1acfc7
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 15, 2026
e829826
Merge branch 'dev' into fix-env-caching-12698
jerome-benoit May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ UPCOMING_CHANGELOG.md
logs/
*.bun-build
tsconfig.tsbuildinfo

# Sisyphus orchestrator state (local only)
.sisyphus/
5 changes: 3 additions & 2 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,8 @@ export const layer = Layer.effect(
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
const url = key.replace(/\/+$/, "")
process.env[value.key] = value.token
// TODO(multi-instance): see env/index.ts docstring.
yield* env.set(value.key, value.token)
log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`))
if (!response.ok) {
Expand Down Expand Up @@ -647,7 +648,7 @@ export const layer = Layer.effect(
{ concurrency: 2 },
)
if (Option.isSome(tokenOpt)) {
process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value
// TODO(multi-instance): see env/index.ts docstring.
yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
}

Expand Down
36 changes: 19 additions & 17 deletions packages/opencode/src/env/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Context, Effect, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"

/**
* Effect-aware wrapper around `process.env`. Reads are live (no snapshot);
* `set`/`remove` mutate `process.env` directly. Writes are process-wide —
* call sites that persist auth-derived values are marked `TODO(multi-instance)`.
*/
type State = Record<string, string | undefined>

export interface Interface {
Expand All @@ -12,23 +16,21 @@ export interface Interface {

export class Service extends Context.Service<Service, Interface>()("@opencode/Env") {}

export const layer = Layer.effect(
export const layer = Layer.succeed(
Service,
Effect.gen(function* () {
const state = yield* InstanceState.make<State>(Effect.fn("Env.state")(() => Effect.succeed({ ...process.env })))

const get = Effect.fn("Env.get")((key: string) => InstanceState.use(state, (env) => env[key]))
const all = Effect.fn("Env.all")(() => InstanceState.get(state))
const set = Effect.fn("Env.set")(function* (key: string, value: string) {
const env = yield* InstanceState.get(state)
env[key] = value
})
const remove = Effect.fn("Env.remove")(function* (key: string) {
const env = yield* InstanceState.get(state)
delete env[key]
})

return Service.of({ get, all, set, remove })
Service.of({
get: (key: string) => Effect.sync(() => process.env[key]),
all: () => Effect.sync(() => ({ ...process.env })),
set: Effect.fn("Env.set")((key: string, value: string) =>
Effect.sync(() => {
process.env[key] = value
}),
),
remove: Effect.fn("Env.remove")((key: string) =>
Effect.sync(() => {
delete process.env[key]
}),
),
}),
)

Expand Down
93 changes: 93 additions & 0 deletions packages/opencode/src/provider/overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Hash } from "@opencode-ai/core/util/hash"

import type { ProviderID } from "@/provider/schema"
import type { Provider } from "@/provider/provider"

interface OverlayState {
cachedProviders: Record<ProviderID, Provider.Info>
cleanedDatabase: Readonly<Record<ProviderID, Provider.Info>>
}

/**
* Pure single-provider env-overlay step. See `overlay.test.ts` for the
* exhaustive precedence table.
*/
export function resolveEnvOverlay(
cached: Provider.Info | undefined,
candidate: Provider.Info,
apiKey: string | undefined,
): Provider.Info | undefined {
if (!apiKey) {
if (cached?.source === "env") return undefined
return cached
}
if (cached && cached.source !== "env") {
if (!cached.key && candidate.env.length === 1) return { ...cached, key: apiKey }
return cached
}
// Multi-env candidate: cached.key has no single source of truth, preserve it.
const nextKey = candidate.env.length === 1 ? apiKey : cached?.key
if (cached && cached.key === nextKey) return cached
if (cached) return { ...cached, key: nextKey }
return { ...candidate, source: "env", key: nextKey }
}

export function currentProviders(
s: OverlayState,
envs: Record<string, string | undefined>,
): Record<ProviderID, Provider.Info> {
const result: Record<ProviderID, Provider.Info> = { ...s.cachedProviders }
for (const [id, info] of Object.entries(s.cleanedDatabase)) {
const providerID = id as ProviderID
// Empty/whitespace env values count as absent. Non-blank values are
// passed through verbatim — trimming a real key would be silently wrong.
const apiKey = info.env.map((k) => envs[k]).find(isNonBlank)
const next = resolveEnvOverlay(result[providerID], info, apiKey)
if (next) result[providerID] = next
else delete result[providerID]
}
return result
}

export function isNonBlank(v: string | undefined): v is string {
return typeof v === "string" && v.trim() !== ""
}

// JSON.stringify drops functions silently and throws on BigInt. Tag both so
// distinct closures (e.g. AWS `coalesceProvider`) and BigInt values produce
// stable, distinct hashes. Anonymous arrows collide on `__fn:anon` — that is
// intentional: the per-call `fetch` wrapper built in `resolveSDK` would
// otherwise bust the SDK cache on every invocation.
//
// CAVEAT: same-named closures from unrelated callers (e.g. a third-party
// plugin storing a `coalesceProvider` in `provider.options`) collide and may
// silently serve a stale SDK. Plugin authors must keep stateful closures out
// of `provider.options` outside the per-call `fetch` convention.
//
// UNSUPPORTED INPUT TYPES (silent collisions or throws — do not place these
// in `provider.options`):
// - `Map`, `Set`, `WeakMap`, `WeakSet` — `JSON.stringify` returns `"{}"`,
// so two distinct instances collide on the same hash.
// - `RegExp` — also serializes to `"{}"`, same collision.
// - `Symbol` — silently dropped by `JSON.stringify` (becomes `undefined`).
// - Circular references — `JSON.stringify` throws `TypeError`, propagated
// as a defect through `getLanguage`/`resolveSDK`.
// - `Buffer` / `Uint8Array` — serialized as `{0:n,1:n,...}`; large but
// distinct, so correct but inefficient.
// - `Date`, `URL` — handled correctly via their `toJSON()` (ISO string,
// `href`).
//
// TODO(hash): swap for `effect/Hash` + `Equal.equals` with WeakMap-tracked
// function identity to fix the named-collision risk and the unsupported
// types above.
export function hashIdentity(parts: Record<string, unknown>): string {
return Hash.fast(
JSON.stringify(parts, (_key, value) => {
if (typeof value === "function") return `__fn:${value.name || "anon"}`
if (typeof value === "bigint") return `${value.toString()}n`
return value
}),
)
}

export * as ProviderOverlay from "./overlay"
Loading
Loading