fix(agent-hooks): don't mirror the XDG default OpenCode config into the WSL overlay

OPENCODE_CONFIG_DIR is APPENDED to OpenCode's config-dir list, not a
replacement for it. Verified against the shipped binary: the list is built as
[Path.config, ...project .opencode dirs, ...OPENCODE_CONFIG_DIR ? [it] : []],
and Path.config is derived independently from XDG_CONFIG_HOME/$HOME/.config.

So ~/.config/opencode is read whether or not Orca overrides the var, and the
earlier fallback that mirrored it into the overlay made OpenCode load the
user's config -- and their plugins -- twice. Resolve only an explicitly-set
dir, which is the one case that genuinely leaves the list when Orca overwrites
the variable. This also restores parity with the SSH and local paths.
This commit is contained in:
OrcaWin
2026-07-24 15:06:28 -07:00
parent a108a2b794
commit 6745eba904
2 changed files with 28 additions and 39 deletions
+22 -20
View File
@@ -63,20 +63,20 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues
})
})
it('rebuilds when a config root appears after the first install', () => {
it('rebuilds when an explicitly-set config root appears after the first install', () => {
withHome((home) => {
const install = createInstallPluginsHandler(new PluginOverlayManager({ homeDir: home }), {
HOME: home,
ORCA_WSL_HOOK_INSTANCE: 'inst1'
} as NodeJS.ProcessEnv)
const userConfig = join(home, 'my-opencode')
const env = { HOME: home, ORCA_WSL_HOOK_INSTANCE: 'inst1' } as NodeJS.ProcessEnv
const install = createInstallPluginsHandler(new PluginOverlayManager({ homeDir: home }), env)
const source = '// v1\n'
install({ opencodePluginSource: source })
// Why: the user runs opencode outside Orca (or creates the dir by hand) after the
// relay connected; keying the cache on the plugin source alone would never mirror it.
const userConfig = join(home, '.config', 'opencode')
// Why: the source dir is resolved per call, so a dir that only becomes
// resolvable later still gets mirrored; keying the cache on the plugin
// source alone would leave the overlay plugin-only for the relay's life.
mkdirSync(userConfig, { recursive: true })
writeFileSync(join(userConfig, 'opencode.json'), '{"model":"late"}')
env.ORCA_OPENCODE_SOURCE_CONFIG_DIR = userConfig
const dir = install({ opencodePluginSource: source }).overlayDirs.opencode as string
expect(readFileSync(join(dir, 'opencode.json'), 'utf8')).toBe('{"model":"late"}')
@@ -128,17 +128,17 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues
})
})
it("mirrors the guest's default config root so it keeps applying in WSL panes", () => {
it('mirrors an explicitly-set config root so overriding the var does not drop it', () => {
withHome((home) => {
// Why: nothing explicit is discoverable from the relay's own env, so without
// this default the overlay would hold only Orca's plugin and the user's real
// OpenCode config would silently stop applying once the pane points at it.
const userConfig = join(home, '.config', 'opencode')
// Why: setting OPENCODE_CONFIG_DIR to the overlay removes the user's own value
// from OpenCode's config-dir list, so that one must be mirrored in.
const userConfig = join(home, 'my-opencode')
mkdirSync(userConfig, { recursive: true })
writeFileSync(join(userConfig, 'opencode.json'), '{"model":"user-set"}')
const install = createInstallPluginsHandler(new PluginOverlayManager({ homeDir: home }), {
HOME: home,
ORCA_OPENCODE_SOURCE_CONFIG_DIR: userConfig,
ORCA_WSL_HOOK_INSTANCE: 'inst1'
} as NodeJS.ProcessEnv)
const dir = install({ opencodePluginSource: '// v1\n' }).overlayDirs.opencode as string
@@ -148,21 +148,23 @@ describe.skipIf(process.platform === 'win32')('createInstallPluginsHandler (gues
})
})
it('honours XDG_CONFIG_HOME for the default config root', () => {
it('does not mirror the XDG default config root', () => {
withHome((home) => {
const xdg = join(home, 'xdg')
const userConfig = join(xdg, 'opencode')
mkdirSync(userConfig, { recursive: true })
writeFileSync(join(userConfig, 'opencode.json'), '{"model":"xdg"}')
// Why: OpenCode APPENDS OPENCODE_CONFIG_DIR to its config-dir list rather than
// replacing it, so ~/.config/opencode is read anyway — mirroring it here would
// load the user's config and plugins twice.
const defaultConfig = join(home, '.config', 'opencode')
mkdirSync(defaultConfig, { recursive: true })
writeFileSync(join(defaultConfig, 'opencode.json'), '{"model":"default"}')
const install = createInstallPluginsHandler(new PluginOverlayManager({ homeDir: home }), {
HOME: home,
XDG_CONFIG_HOME: xdg,
ORCA_WSL_HOOK_INSTANCE: 'inst1'
} as NodeJS.ProcessEnv)
const dir = install({ opencodePluginSource: '// v1\n' }).overlayDirs.opencode as string
expect(readFileSync(join(dir, 'opencode.json'), 'utf8')).toBe('{"model":"xdg"}')
expect(existsSync(join(dir, 'opencode.json'))).toBe(false)
expect(existsSync(join(dir, 'plugins', 'orca-opencode-status.js'))).toBe(true)
})
})
+6 -19
View File
@@ -4,7 +4,6 @@
// unit-testable without binding the hook server. Scope is OpenCode only for
// now; the payload/response shape matches the SSH relay so Pi/OMP are additive.
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { getRelayOpenCodePluginPath, type PluginOverlayManager } from './plugin-overlay'
import { resolveOpenCodeSourceConfigDir } from './plugin-overlay-env'
@@ -21,23 +20,11 @@ export type InstallPluginsResult = {
export type InstallPluginsHandler = (params: Record<string, unknown>) => InstallPluginsResult
function resolveGuestOpenCodeConfigDir(env: NodeJS.ProcessEnv): string | undefined {
const explicit = resolveOpenCodeSourceConfigDir(env as Record<string, string>, env.SHELL)
if (explicit) {
return explicit
}
// Why: nothing explicit is normally discoverable from the relay's own env, and
// without a source dir the overlay holds ONLY Orca's plugin — so pointing
// OPENCODE_CONFIG_DIR at it would stop the guest user's real ~/.config/opencode
// (models, agents, skills, mcp) from applying in WSL panes, where it applied before.
const home = env.HOME
if (!home) {
return undefined
}
const defaultDir = join(env.XDG_CONFIG_HOME || join(home, '.config'), 'opencode')
return existsSync(defaultDir) ? defaultDir : undefined
}
// Why NOT to fall back to ~/.config/opencode here: OpenCode APPENDS
// OPENCODE_CONFIG_DIR to its config-dir list, it does not replace it — the
// XDG default is always read too. Mirroring the default into the overlay would
// load the user's config (and plugins) twice. Only an explicitly-set dir is
// mirrored, because that one leaves the list when we override the var.
export function createInstallPluginsHandler(
pluginOverlay: PluginOverlayManager,
env: NodeJS.ProcessEnv
@@ -69,7 +56,7 @@ export function createInstallPluginsHandler(
const incoming = typeof opencode === 'string' ? opencode : null
// Re-resolved every call so a config root created after the first install still
// gets mirrored; the rc scan is memoized and the default branch is one existsSync.
const sourceDir = resolveGuestOpenCodeConfigDir(env)
const sourceDir = resolveOpenCodeSourceConfigDir(env as Record<string, string>, env.SHELL)
const cached = materialized
if (
cached &&