fix(opencode): keep Go credentials private and resolve backend keys (#24615)

Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth.

Original-topic-commit: 7903f1cddb
Original-topic-commit: 588117b5cf
Original-topic-commit: dedd4f8c86
Original-topic-commit: 6645dae104
Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-onto: b032867021

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
Reported-by: pullfrog
Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f
Native-helper-source: 80dbe23237


Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c
This commit is contained in:
Neil
2026-10-02 23:21:27 -07:00
committed by GitHub
co-authored by kespineira kevimux
parent e8402b4619
commit 53f9ea7839
55 changed files with 2271 additions and 301 deletions
@@ -0,0 +1,182 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { runProcess } from '../../shared/child-process/run-process'
import { resolveCommandOnLocalPath } from '../ipc/command-path-resolver'
import {
detectOpenCodeCredentialBackend,
resetOpenCodeCredentialBackendProbes
} from './opencode-credential-backend'
const files = vi.hoisted(() => ({ realpath: vi.fn(), stat: vi.fn() }))
vi.mock('../../shared/child-process/run-process', () => ({ runProcess: vi.fn() }))
vi.mock('../ipc/command-path-resolver', () => ({ resolveCommandOnLocalPath: vi.fn() }))
vi.mock('node:fs/promises', () => files)
describe('OpenCode credential execution backend', () => {
beforeEach(() => {
vi.resetAllMocks()
resetOpenCodeCredentialBackendProbes()
files.realpath.mockImplementation(async (path: string) => path)
files.stat.mockResolvedValue({ dev: 1, ino: 2, size: 3, mtimeMs: 4, ctimeMs: 5 })
vi.mocked(resolveCommandOnLocalPath).mockResolvedValue('/task/bin/opencode')
vi.mocked(runProcess).mockResolvedValue({
code: 0,
signal: null,
stdout: '1.18.30\n',
stderr: '',
timedOut: false
})
})
it.each([
['1.18.30\n', 'v1'],
['opencode v2.0.16\n', 'v2'],
['2.0.16', 'v2'],
['opencode v2.0.16-beta.1', 'v2'],
['3.0.0', null],
['wrapper 2.0.16', null],
['', null]
])('uses the reported backend for %j', async (stdout, backend) => {
vi.mocked(runProcess).mockResolvedValue({
code: 0,
signal: null,
stdout,
stderr: '',
timedOut: false
})
expect(await detectOpenCodeCredentialBackend()).toBe(backend)
})
it('resolves and executes the binary in the caller environment with a bounded probe', async () => {
const environment = { PATH: '/task/bin', XDG_DATA_HOME: '/task/data' }
await detectOpenCodeCredentialBackend(environment, '/task/workspace')
expect(resolveCommandOnLocalPath).toHaveBeenCalledExactlyOnceWith('opencode', {
env: environment,
cwd: '/task/workspace'
})
expect(runProcess).toHaveBeenCalledExactlyOnceWith({
program: '/task/bin/opencode',
args: ['--version'],
env: environment,
cwd: '/task/workspace',
timeoutMs: 5_000,
maxOutputBytes: 1_024
})
})
it('probes opencode2 only when the default opencode command is absent', async () => {
vi.mocked(resolveCommandOnLocalPath)
.mockResolvedValueOnce(null)
.mockResolvedValueOnce('/task/bin/opencode2')
vi.mocked(runProcess).mockResolvedValue({
code: 0,
signal: null,
stdout: 'opencode v2.0.16',
stderr: '',
timedOut: false
})
expect(await detectOpenCodeCredentialBackend()).toBe('v2')
expect(runProcess).toHaveBeenCalledWith(
expect.objectContaining({ program: '/task/bin/opencode2' })
)
})
it.each([
{ code: 1, timedOut: false },
{ code: 0, timedOut: true },
{ code: 0, timedOut: false, outputTruncated: true }
])('withholds authority when the installed probe fails: %j', async (failure) => {
vi.mocked(runProcess).mockResolvedValue({
...failure,
signal: null,
stdout: 'opencode v2.0.16',
stderr: ''
})
expect(await detectOpenCodeCredentialBackend()).toBeNull()
expect(resolveCommandOnLocalPath).toHaveBeenCalledTimes(1)
})
it('withholds authority after a spawn error without substituting another CLI', async () => {
vi.mocked(runProcess).mockRejectedValue(new Error('unavailable'))
expect(await detectOpenCodeCredentialBackend()).toBeNull()
expect(resolveCommandOnLocalPath).toHaveBeenCalledTimes(1)
})
it('withholds authority when neither CLI is installed', async () => {
vi.mocked(resolveCommandOnLocalPath).mockResolvedValue(null)
expect(await detectOpenCodeCredentialBackend()).toBeNull()
expect(runProcess).not.toHaveBeenCalled()
})
it('coalesces concurrent calls and reuses a successful binary identity', async () => {
expect(
await Promise.all([
detectOpenCodeCredentialBackend(),
detectOpenCodeCredentialBackend(),
detectOpenCodeCredentialBackend()
])
).toEqual(['v1', 'v1', 'v1'])
expect(await detectOpenCodeCredentialBackend()).toBe('v1')
expect(runProcess).toHaveBeenCalledTimes(1)
})
it('reprobes when the resolved binary is replaced', async () => {
expect(await detectOpenCodeCredentialBackend()).toBe('v1')
files.stat.mockResolvedValue({ dev: 1, ino: 6, size: 3, mtimeMs: 7, ctimeMs: 8 })
vi.mocked(runProcess).mockResolvedValue({
code: 0,
signal: null,
stdout: 'opencode v2.0.16',
stderr: '',
timedOut: false
})
expect(await detectOpenCodeCredentialBackend()).toBe('v2')
expect(runProcess).toHaveBeenCalledTimes(2)
})
it('does not reuse a probe across caller environments or working directories', async () => {
await detectOpenCodeCredentialBackend(
{ PATH: '/task/bin', XDG_DATA_HOME: '/task/a' },
'/task/a'
)
await detectOpenCodeCredentialBackend(
{ PATH: '/task/bin', XDG_DATA_HOME: '/task/b' },
'/task/a'
)
await detectOpenCodeCredentialBackend(
{ PATH: '/task/bin', XDG_DATA_HOME: '/task/b' },
'/task/b'
)
expect(runProcess).toHaveBeenCalledTimes(3)
})
it('retries an unknown backend after its short cache expires', async () => {
const now = vi.spyOn(Date, 'now').mockReturnValue(1_000)
vi.mocked(runProcess).mockRejectedValueOnce(new Error('temporarily unavailable'))
try {
expect(await detectOpenCodeCredentialBackend()).toBeNull()
expect(await detectOpenCodeCredentialBackend()).toBeNull()
now.mockReturnValue(6_001)
expect(await detectOpenCodeCredentialBackend()).toBe('v1')
expect(runProcess).toHaveBeenCalledTimes(2)
} finally {
now.mockRestore()
}
})
it('withholds authority when the selected binary identity cannot be read', async () => {
files.stat.mockRejectedValue(new Error('unreadable executable'))
expect(await detectOpenCodeCredentialBackend()).toBeNull()
expect(runProcess).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,91 @@
import { runProcess } from '../../shared/child-process/run-process'
import { resolveCommandOnLocalPath } from '../ipc/command-path-resolver'
import { createHash } from 'node:crypto'
import { realpath, stat } from 'node:fs/promises'
export type OpenCodeCredentialBackend = 'v1' | 'v2'
// Native runtime processes own separate execution-host caches.
const probes = new Map<
string,
{ result: Promise<OpenCodeCredentialBackend | null>; expiresAt: number }
>()
const MAX_PROBES = 32
export function resetOpenCodeCredentialBackendProbes(): void {
probes.clear()
}
export async function detectOpenCodeCredentialBackend(
environment: NodeJS.ProcessEnv = process.env,
cwd = process.cwd()
): Promise<OpenCodeCredentialBackend | null> {
// The execution host's default CLI owns this lookup; table presence proves neither backend.
const program =
(await resolveCommandOnLocalPath('opencode', { env: environment, cwd })) ??
(await resolveCommandOnLocalPath('opencode2', { env: environment, cwd }))
if (!program) {
return null
}
try {
const binary = await realpath(program)
const identity = await stat(binary)
const environmentDigest = createHash('sha256')
.update(JSON.stringify(Object.entries(environment).sort(([a], [b]) => a.localeCompare(b))))
.digest('hex')
const key = JSON.stringify([
binary,
identity.dev,
identity.ino,
identity.size,
identity.mtimeMs,
identity.ctimeMs,
cwd,
environmentDigest
])
const cached = probes.get(key)
if (cached && cached.expiresAt > Date.now()) {
return cached.result
}
const result = probeBackend(binary, environment, cwd)
const entry = { result, expiresAt: Number.POSITIVE_INFINITY }
probes.set(key, entry)
if (probes.size > MAX_PROBES) {
const oldest = probes.keys().next().value
if (oldest !== undefined) {
probes.delete(oldest)
}
}
const backend = await result
entry.expiresAt = Date.now() + (backend ? 60_000 : 5_000)
return backend
} catch {
return null
}
}
async function probeBackend(
program: string,
environment: NodeJS.ProcessEnv,
cwd: string
): Promise<OpenCodeCredentialBackend | null> {
try {
const result = await runProcess({
program,
args: ['--version'],
env: environment,
cwd,
timeoutMs: 5_000,
maxOutputBytes: 1_024
})
if (result.code !== 0 || result.timedOut || result.outputTruncated) {
return null
}
const version = /^(?:opencode\s+)?v?([12])\.\d+\.\d+(?:[-+][\w.-]+)?$/i.exec(
result.stdout.trim()
)
return version?.[1] === '1' ? 'v1' : version?.[1] === '2' ? 'v2' : null
} catch {
return null
}
}
@@ -0,0 +1,99 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import type * as NodeFs from 'node:fs'
import type * as NodeOs from 'node:os'
import { join } from 'node:path'
const home = vi.hoisted(() => {
const state: { directory: string; readError: Error | null } = { directory: '', readError: null }
return state
})
vi.mock('node:os', async (importOriginal) => ({
...(await importOriginal<typeof NodeOs>()),
homedir: () => home.directory
}))
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof NodeFs>()
return {
...actual,
readFileSync: (...args: Parameters<typeof actual.readFileSync>) => {
if (home.readError) {
throw home.readError
}
return actual.readFileSync(...args)
}
}
})
vi.mock('electron', () => ({
safeStorage: {
isEncryptionAvailable: () => true,
encryptString: (key: string) => Buffer.from(`encrypted:${key}`),
decryptString: (bytes: Buffer) => bytes.toString().slice('encrypted:'.length)
}
}))
beforeEach(() => {
home.readError = null
home.directory = mkdtempSync(join(tmpdir(), 'orca-go-key-store-'))
vi.resetModules()
})
afterEach(() => rmSync(home.directory, { recursive: true, force: true }))
describe('OpenCode Go main-owned API key file', () => {
it('persists a versioned encrypted envelope, reads it after restart, and clears it', async () => {
const store = await import('./opencode-go-api-key-store')
expect(store.hasOpenCodeGoApiKey()).toBe(false)
store.saveOpenCodeGoApiKey(' fake-key ')
expect(store.hasOpenCodeGoApiKey()).toBe(true)
const path = join(home.directory, '.orca', 'opencode-go-api-key.enc')
expect(readFileSync(path, 'utf8')).toBe(
`orca-opencode-go-api-key:v1:encrypted:${Buffer.from('encrypted:fake-key').toString('base64')}`
)
vi.resetModules()
const restarted = await import('./opencode-go-api-key-store')
expect(restarted.readOpenCodeGoApiKey()).toBe('fake-key')
restarted.clearOpenCodeGoApiKey()
expect(restarted.hasOpenCodeGoApiKey()).toBe(false)
expect(restarted.readOpenCodeGoApiKey()).toBeNull()
})
it('keeps the MiniMax cache and file independent', async () => {
const go = await import('./opencode-go-api-key-store')
const miniMax = await import('../minimax/minimax-api-key-store')
go.saveOpenCodeGoApiKey('fake-go')
miniMax.saveMiniMaxApiKey('fake-minimax')
expect(go.readOpenCodeGoApiKey()).toBe('fake-go')
expect(miniMax.readMiniMaxApiKey()).toBe('fake-minimax')
go.clearOpenCodeGoApiKey()
expect(miniMax.hasMiniMaxApiKey()).toBe(true)
expect(miniMax.readMiniMaxApiKey()).toBe('fake-minimax')
})
it('rejects empty keys and malformed envelopes without returning the key', async () => {
const store = await import('./opencode-go-api-key-store')
expect(() => store.saveOpenCodeGoApiKey(' ')).toThrow('required')
store.saveOpenCodeGoApiKey('fake-key')
writeFileSync(join(home.directory, '.orca', 'opencode-go-api-key.enc'), 'fake-invalid-envelope')
vi.resetModules()
const restarted = await import('./opencode-go-api-key-store')
expect(() => restarted.readOpenCodeGoApiKey()).toThrow(
'OpenCode Go API key could not be decrypted'
)
})
it('throws a distinct unreadable error for a transient read failure', async () => {
const store = await import('./opencode-go-api-key-store')
store.saveOpenCodeGoApiKey('fake-key')
vi.resetModules()
const restarted = await import('./opencode-go-api-key-store')
vi.spyOn(console, 'warn').mockImplementation(() => {})
home.readError = Object.assign(new Error('resource busy'), { code: 'EBUSY' })
expect(() => restarted.readOpenCodeGoApiKey()).toThrow(
'OpenCode Go API key file could not be read'
)
home.readError = null
expect(restarted.readOpenCodeGoApiKey()).toBe('fake-key')
})
})
@@ -0,0 +1,13 @@
import { createEncryptedApiKeyFileStore } from '../credentials/encrypted-api-key-file-store'
const store = createEncryptedApiKeyFileStore({
fileName: 'opencode-go-api-key.enc',
envelopePrefix: 'orca-opencode-go-api-key:v1:',
providerLabel: 'OpenCode Go',
logScope: 'opencode-go'
})
export const hasOpenCodeGoApiKey = store.has
export const saveOpenCodeGoApiKey = store.save
export const readOpenCodeGoApiKey = store.read
export const clearOpenCodeGoApiKey = store.clear