Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f98f17e
Add host-owned OpenCode and Devin account profiles
nwparker Oct 2, 2026
297d6f3
Manage OpenCode and Devin profiles in account Settings
nwparker Oct 2, 2026
3e7e608
Expose registered account roots to host transcript readers
nwparker Oct 2, 2026
ea87b36
Clarify managed profile provider flags
nwparker Oct 2, 2026
c89ecf1
Retain isolated Electron home in browser sidecars
nwparker Oct 2, 2026
4f9c569
Restore inherited account environment and preserve cleanup retries
nwparker Oct 2, 2026
889e4aa
Check relay environment values before merging
nwparker Oct 2, 2026
7903f1c
fix(opencode): keep saved Go credentials in main-owned storage
nwparker Oct 2, 2026
588117b
test(opencode): retain legacy key ownership across worker writes
nwparker Oct 2, 2026
d1abb60
Consolidate managed account type imports
nwparker Oct 2, 2026
dedd4f8
test(accounts): provide OpenCode credential API in lifetime fixture
nwparker Oct 2, 2026
d10f8b0
fix(accounts): use existing localized provider names
nwparker Oct 2, 2026
8a95b0c
Keep managed account baselines private to the execution host
nwparker Oct 2, 2026
c49896e
Align account enrollment help with accepted providers and flags
nwparker Oct 2, 2026
ed29c53
Show the active System account in managed profile lists
nwparker Oct 2, 2026
4d92716
Document the validated Linux managed account scope
nwparker Oct 2, 2026
6645dae
fix(opencode): resolve Go keys by execution backend
nwparker Oct 2, 2026
1388ce6
Merge current main into managed account corrections
nwparker Oct 2, 2026
1c5b940
Combine reviewed host-private accounts and backend credential authori…
nwparker Oct 2, 2026
2a8a26c
Use host-private account restoration for OpenCode Go usage
nwparker Oct 2, 2026
fa9f6a0
Fix managed account removal, credential audits, and runtime bundling
nwparker Oct 3, 2026
3c62b4a
Quarantine removed accounts and audit captured credential snapshots
nwparker Oct 3, 2026
e45242b
Merge current main into corrected managed account profiles
nwparker Oct 3, 2026
8913157
Integrate managed OpenCode and Devin accounts with current main and n…
nwparker Oct 3, 2026
cf71ae4
fix(opencode): preserve managed profiles and database ownership
nwparker Oct 3, 2026
1431547
fix(accounts): canonicalize account removal to rm
nwparker Oct 3, 2026
6abaf73
fix(opencode): retain managed accounts with canonical removal commands
nwparker Oct 3, 2026
c2fb8c8
fix(accounts): recover interrupted profile removal on startup
nwparker Oct 3, 2026
28739cd
fix(accounts): protect registered UUID case variants during removal r…
nwparker Oct 3, 2026
e8e2e22
Recover interrupted managed account removal and protect registered UU…
nwparker Oct 3, 2026
78d54c5
Use the selected execution-host account for OpenCode Go credential lo…
nwparker Oct 3, 2026
70bc58d
Allow cold node-pty setup on Windows ARM CI
nwparker Oct 3, 2026
2cd88b1
Restack managed data accounts with bounded Windows ARM native prepara…
nwparker Oct 3, 2026
17d2016
Restack complete managed data accounts on merged Windows ARM preparat…
nwparker Oct 3, 2026
17f2ab6
Preserve selected-account lookup on current full account fix
nwparker Oct 3, 2026
484e9ff
Retain reviewed OpenCode account source on current compatible main
nwparker Oct 3, 2026
268d10d
Retain reviewed OpenCode account source on current compatible main
nwparker Oct 3, 2026
7a4d4a0
Compose managed OpenCode accounts with current relay token ownership
nwparker Oct 3, 2026
1845f0b
Preserve selected OpenCode account key lookup on composed main
nwparker Oct 3, 2026
40019f4
Retain reviewed managed accounts on CSP-compatible main
nwparker Oct 3, 2026
a3ab2a9
Retain selected OpenCode account caller on CSP-compatible main
nwparker Oct 3, 2026
2d90030
fix(opencode): preserve selected-account caller on merged account main
nwparker Oct 3, 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
8 changes: 7 additions & 1 deletion src/main/rate-limits/opencode-go-api-key-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down
241 changes: 241 additions & 0 deletions src/main/rate-limits/opencode-go-managed-account-source.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
Loading