Read OpenCode Go usage from the selected account (#24770)

* Add host-owned OpenCode and Devin account profiles

* Manage OpenCode and Devin profiles in account Settings

* Expose registered account roots to host transcript readers

* Clarify managed profile provider flags

* Retain isolated Electron home in browser sidecars

* Restore inherited account environment and preserve cleanup retries

* Check relay environment values before merging

* fix(opencode): keep saved Go credentials in main-owned storage

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>

* test(opencode): retain legacy key ownership across worker writes

* Consolidate managed account type imports

* test(accounts): provide OpenCode credential API in lifetime fixture

* fix(accounts): use existing localized provider names

Align the new Japanese account copy with the existing catalog repair policy.

* Keep managed account baselines private to the execution host

* Align account enrollment help with accepted providers and flags

* Show the active System account in managed profile lists

* Document the validated Linux managed account scope

* fix(opencode): resolve Go keys by execution backend

* Use host-private account restoration for OpenCode Go usage

* Fix managed account removal, credential audits, and runtime bundling

* Quarantine removed accounts and audit captured credential snapshots

* fix(accounts): canonicalize account removal to rm

Use account rm as the canonical removal command and keep account remove as an alias. Preserve the existing accounts.removeData RPC and the full original 63-path account component.

Original-Account-Source: cf71ae4cb6
Frozen-Account-Base: 08ee7ba9ef
Frozen-Integrated-Main: 53f9ea7839
Private validation source only; no ref or publication.

* fix(accounts): recover interrupted profile removal on startup

Scan private removal backups before quarantined cleanup, reuse the existing state schema to validate the exact UUID, and preserve registered or unmarked profiles. Mark account rm as destructive using the existing typo recovery policy. Retain the full original account component and public author ancestry.

Account-PR: 24636
Original-Account-Source: cf71ae4cb6
Reviewed-Public-Parent: 6abaf736e3
Frozen-Integrated-Main: 53f9ea7839
Private source only; no ref or publication.

* fix(accounts): protect registered UUID case variants during removal recovery

Compare validated account UUID identities without changing stored IDs or filesystem paths. Protect registered originals and quarantines, including explicit retry variants, and accept equivalent UUID spelling in a valid removal backup. Retain the complete account component and published contributor ancestry.

Private source only; no index, ref, or public mutation.

* Allow cold node-pty setup on Windows ARM CI

Keep a ten-minute bound only for node-pty rebuilt on Windows ARM CI hosts; all other node-gyp calls retain five minutes. Cold setup in two completed ARM jobs left compilation less than a minute.

---------

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-03 09:19:06 -07:00
committed by GitHub
co-authored by kespineira kevimux
parent c2dba12072
commit 786a040b96
2 changed files with 248 additions and 1 deletions
@@ -8,6 +8,7 @@ import { tableExists } from '../opencode-usage/schema-helpers'
import { isWslUncPath } from '../../shared/wsl-paths'
import { resolveOpenCodeDataDirectory } from '../opencode/opencode-data-directory'
import Database from '../sqlite/sync-database'
import { getManagedDataAccountService } from '../managed-data-accounts/service'
import {
detectOpenCodeCredentialBackend,
type OpenCodeCredentialBackend
@@ -233,7 +234,12 @@ export async function resolveOpenCodeGoApiKey(input: {
if (override) {
return { status: 'found', key: override, tier: 'settings' }
}
const environment = input.environment ?? process.env
const environment = input.environment ?? { ...process.env }
if (!input.environment) {
const accounts = getManagedDataAccountService()
accounts.restoreOriginalEnvironment(environment)
Object.assign(environment, accounts.launchEnvironment('opencode'))
}
const backend = input.backend ?? (await detectOpenCodeCredentialBackend(environment, input.cwd))
let databaseUnreadable = false
if (backend === 'v2') {
@@ -0,0 +1,241 @@
import { randomUUID } from 'node:crypto'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getAppEnvironment, setAppEnvironment } from '../../shared/app-environment'
import { getManagedDataAccountService } from '../managed-data-accounts/service'
import { detectOpenCodeCredentialBackend } from '../opencode/opencode-credential-backend'
import Database from '../sqlite/sync-database'
import { resolveOpenCodeGoApiKey } from './opencode-go-api-key-source'
vi.mock('../opencode/opencode-credential-backend', () => ({
detectOpenCodeCredentialBackend: vi.fn()
}))
let root: string
let systemDataHome: string
let metadataPath: string
function writeCredentials(dataHome: string, name: string): void {
const directory = join(dataHome, 'opencode')
mkdirSync(directory, { recursive: true })
writeFileSync(
join(directory, 'auth.json'),
JSON.stringify({ 'opencode-go': { type: 'api', key: `${name}-auth-placeholder` } })
)
const database = new Database(join(directory, 'opencode.db'))
try {
database.exec(
'CREATE TABLE session (id TEXT); ' +
'CREATE TABLE credential (integration_id TEXT, value TEXT, active INTEGER, time_created INTEGER)'
)
database
.prepare('INSERT INTO credential VALUES (?, ?, 1, 1)')
.run('opencode-go', JSON.stringify({ type: 'key', key: `${name}-table-placeholder` }))
} finally {
database.close()
}
}
async function enrollSelectedAccount(): Promise<string> {
const accounts = getManagedDataAccountService()
const source = join(root, 'source')
writeCredentials(source, 'selected')
const state = await accounts.add('opencode', source, 'Selected')
const selected = accounts.launchEnvironment('opencode')
writeFileSync(
join(selected.XDG_DATA_HOME, 'opencode', 'auth.json'),
readFileSync(join(source, 'opencode', 'auth.json'))
)
await accounts.select('opencode', null)
return state.accounts[0].id
}
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-go-managed-caller-'))
systemDataHome = join(root, 'system-data')
const original = getAppEnvironment()
setAppEnvironment({
...original,
getPath: (name) => (name === 'userData' ? root : original.getPath(name))
})
metadataPath = join(root, 'managed-data-accounts', 'opencode', 'accounts.json')
for (const key of Object.keys(process.env)) {
if (key.startsWith('ORCA_DATA_ACCOUNT_')) {
vi.stubEnv(key, undefined)
}
}
vi.stubEnv('HOME', join(root, 'home'))
vi.stubEnv('XDG_DATA_HOME', systemDataHome)
vi.stubEnv('XDG_STATE_HOME', join(root, 'system-state'))
vi.stubEnv('XDG_CONFIG_HOME', join(root, 'config'))
vi.stubEnv('XDG_CACHE_HOME', join(root, 'cache'))
vi.stubEnv('OPENCODE_DB', 'opencode.db')
vi.stubEnv('OPENCODE_AUTH_CONTENT', undefined)
vi.stubEnv('OPENCODE_API_KEY', undefined)
vi.mocked(detectOpenCodeCredentialBackend).mockReset()
writeCredentials(systemDataHome, 'system')
})
afterEach(() => {
vi.restoreAllMocks()
getManagedDataAccountService().clearInlineAuthBaselines()
vi.unstubAllEnvs()
rmSync(root, { recursive: true, force: true })
})
describe('OpenCode Go execution-host managed account lookup', () => {
it.each(['v1', 'v2'] as const)(
'uses System → selected → System credentials on %s',
async (backend) => {
vi.mocked(detectOpenCodeCredentialBackend).mockResolvedValue(backend)
const id = await enrollSelectedAccount()
const accounts = getManagedDataAccountService()
const inherited = { ...process.env }
const tier = backend === 'v1' ? 'opencode-auth-file' : 'opencode-credential-database'
const suffix = backend === 'v1' ? 'auth' : 'table'
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
status: 'found',
key: `system-${suffix}-placeholder`,
tier
})
await accounts.select('opencode', id)
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
status: 'found',
key: `selected-${suffix}-placeholder`,
tier
})
expect(detectOpenCodeCredentialBackend).toHaveBeenLastCalledWith(
expect.objectContaining(accounts.launchEnvironment('opencode')),
undefined
)
await accounts.select('opencode', null)
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
status: 'found',
key: `system-${suffix}-placeholder`,
tier
})
expect(process.env).toEqual(inherited)
}
)
it.each(['v1', 'v2'] as const)(
'restores an opaque inline System baseline through the host service on %s',
async (backend) => {
vi.mocked(detectOpenCodeCredentialBackend).mockResolvedValue(backend)
const id = await enrollSelectedAccount()
const accounts = getManagedDataAccountService()
await accounts.select('opencode', id)
const selected = accounts.launchEnvironment('opencode')
const inline = JSON.stringify({
'opencode-go': { type: 'api', key: 'system-inline-placeholder' }
})
const baseline: Record<string, string> = {
XDG_DATA_HOME: systemDataHome,
XDG_STATE_HOME: join(root, 'system-state'),
OPENCODE_DB: 'opencode.db',
OPENCODE_AUTH_CONTENT: inline
}
accounts.captureOriginalEnvironment(baseline, selected)
const inheritedProfile: Record<string, string> = {
...baseline,
...selected,
ORCA_DATA_ACCOUNT_PROVIDER: 'opencode',
ORCA_DATA_ACCOUNT_DATA_HOME: selected.XDG_DATA_HOME,
ORCA_DATA_ACCOUNT_STATE_HOME: selected.XDG_STATE_HOME
}
expect(inheritedProfile.ORCA_DATA_ACCOUNT_ORIGINAL_ENV).not.toContain(
'system-inline-placeholder'
)
expect(JSON.parse(baseline.ORCA_DATA_ACCOUNT_ORIGINAL_ENV)).toMatchObject({
OPENCODE_AUTH_CONTENT: null,
inlineAuthReference: expect.any(String)
})
for (const [key, value] of Object.entries(inheritedProfile)) {
vi.stubEnv(key, value)
}
const inherited = { ...process.env }
await accounts.select('opencode', null)
const expected =
backend === 'v1'
? { status: 'found', key: 'system-inline-placeholder', tier: 'opencode-auth-content' }
: {
status: 'found',
key: 'system-table-placeholder',
tier: 'opencode-credential-database'
}
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual(expected)
await accounts.select('opencode', id)
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
status: 'found',
key: backend === 'v1' ? 'selected-auth-placeholder' : 'selected-table-placeholder',
tier: backend === 'v1' ? 'opencode-auth-file' : 'opencode-credential-database'
})
await accounts.select('opencode', null)
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual(expected)
expect(process.env).toEqual(inherited)
}
)
it.each(['malformed', 'unknown', 'unreadable'] as const)(
'rejects %s metadata before probing or falling back, while a manual key still wins',
async (failure) => {
mkdirSync(join(root, 'managed-data-accounts', 'opencode'), { recursive: true })
if (failure === 'unreadable') {
mkdirSync(metadataPath)
} else {
writeFileSync(
metadataPath,
failure === 'malformed'
? '{invalid'
: JSON.stringify({
accounts: [],
activeAccountId: randomUUID()
})
)
}
const before = failure === 'unreadable' ? null : readFileSync(metadataPath, 'utf8')
const accounts = getManagedDataAccountService()
const restore = vi.spyOn(accounts, 'restoreOriginalEnvironment')
const selection = vi.spyOn(accounts, 'launchEnvironment')
const inherited = { ...process.env }
await expect(
resolveOpenCodeGoApiKey({ settingsOverride: ' manual-placeholder ' })
).resolves.toEqual({
status: 'found',
key: 'manual-placeholder',
tier: 'settings'
})
expect(restore).not.toHaveBeenCalled()
expect(selection).not.toHaveBeenCalled()
expect(process.env).toEqual(inherited)
await expect(resolveOpenCodeGoApiKey({})).rejects.toThrow()
expect(detectOpenCodeCredentialBackend).not.toHaveBeenCalled()
if (before !== null) {
expect(readFileSync(metadataPath, 'utf8')).toBe(before)
}
expect(process.env).toEqual(inherited)
}
)
it('returns a manual key without resolving the host service or mutating the inherited environment', async () => {
const inherited = { ...process.env }
vi.spyOn(getAppEnvironment(), 'getPath').mockImplementation(() => {
throw new Error('Host metadata must not be resolved')
})
await expect(
resolveOpenCodeGoApiKey({ settingsOverride: 'manual-placeholder' })
).resolves.toEqual({
status: 'found',
key: 'manual-placeholder',
tier: 'settings'
})
expect(detectOpenCodeCredentialBackend).not.toHaveBeenCalled()
expect(process.env).toEqual(inherited)
vi.restoreAllMocks()
})
})