fix: reuse bundled agent runtime resources

This commit is contained in:
mesalogo
2026-09-07 16:57:59 +08:00
parent d3947c79dd
commit 19086d656e
52 changed files with 1657 additions and 867 deletions
+155 -10
View File
@@ -9,13 +9,17 @@ import {
import spawn from 'cross-spawn'
import { createHash, randomBytes } from 'node:crypto'
import {
cp,
mkdir,
mkdtemp,
readFile,
rename,
rm,
stat,
writeFile
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { dirname, join, resolve } from 'node:path'
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
import type {
AgentQuestionAnswer,
@@ -173,6 +177,9 @@ type OpenCodeSkillRegistration = {
skillsRoot: string
}
const sharedSkillPreparations = new Map<string, Promise<string>>()
const sharedConfigPreparations = new Map<string, Promise<void>>()
const executePermissionRules: PermissionRuleset = [
{ permission: '*', pattern: '*', action: 'allow' }
]
@@ -499,11 +506,13 @@ export type OpenCodeRuntimeOptions = {
embedded: boolean
binaryPath: string
bundledBinaryPath?: string
bundledConfigPath?: string
configPath: string
defaultWorkspace: string
modelProfile?: ResolvedModelProfile
skillInstructions?: string
skillPackages?: RuntimeSkillPackage[]
sharedCacheRoot?: string
knowledgeGateway?: KnowledgeMcpGateway
mcpServers?: ResolvedMcpServer[]
customization?: RuntimeCustomizationSettings['opencode']
@@ -851,21 +860,101 @@ export class OpenCodeRuntime implements AgentRuntime {
return ids
}
private async normalizeSkillPackages(skillsRoot: string): Promise<void> {
for (const skill of this.options.skillPackages ?? []) {
await normalizeOpenCodeSkillManifest(
join(skillsRoot, skill.id),
skill.id
)
}
}
private async prepareSharedSkills(sharedRoot: string): Promise<string> {
const packages = this.options.skillPackages ?? []
const fingerprint = createHash('sha256')
.update(
packages
.map((skill) => ({
id: skill.id,
digest: skill.digest ?? resolve(skill.directory)
}))
.sort((left, right) => left.id.localeCompare(right.id))
.map((skill) => `${skill.id}\0${skill.digest}`)
.join('\0')
)
.digest('hex')
const cacheRoot = join(sharedRoot, 'skills')
const target = join(cacheRoot, fingerprint)
const existing = sharedSkillPreparations.get(target)
if (existing) {
return existing
}
const preparation = (async () => {
try {
if ((await stat(target)).isDirectory()) {
return target
}
} catch {
// The immutable Skill snapshot has not been prepared yet.
}
await mkdir(cacheRoot, { recursive: true, mode: 0o700 })
const stagingRoot = await mkdtemp(join(cacheRoot, '.staging-'))
try {
const staged = await stageRuntimeSkillPackages(
stagingRoot,
packages,
'OpenCode'
)
await this.normalizeSkillPackages(staged)
try {
await rename(staged, target)
} catch (error) {
try {
if ((await stat(target)).isDirectory()) {
return target
}
} catch {
// Preserve the original rename failure below.
}
throw error
}
return target
} finally {
await rm(stagingRoot, { recursive: true, force: true })
}
})()
sharedSkillPreparations.set(target, preparation)
try {
return await preparation
} catch (error) {
sharedSkillPreparations.delete(target)
throw error
}
}
private async createSkillRegistration(): Promise<OpenCodeSkillRegistration> {
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-opencode-'))
const configDirectory = join(root, 'config')
try {
const sharedRoot = this.options.sharedCacheRoot?.trim()
if (sharedRoot) {
const configDirectory = join(
resolve(sharedRoot),
'config',
'opencode'
)
await this.prepareSharedConfig(configDirectory)
const skillsRoot = await this.prepareSharedSkills(
resolve(sharedRoot)
)
return { root, configDirectory, skillsRoot }
}
const configDirectory = join(root, 'config')
const skillsRoot = await stageRuntimeSkillPackages(
configDirectory,
this.options.skillPackages ?? [],
'OpenCode'
)
for (const skill of this.options.skillPackages ?? []) {
await normalizeOpenCodeSkillManifest(
join(skillsRoot, skill.id),
skill.id
)
}
await this.normalizeSkillPackages(skillsRoot)
return { root, configDirectory, skillsRoot }
} catch (error) {
await rm(root, { recursive: true, force: true })
@@ -873,6 +962,57 @@ export class OpenCodeRuntime implements AgentRuntime {
}
}
private async prepareSharedConfig(
configDirectory: string
): Promise<void> {
const bundledConfigPath = this.options.bundledConfigPath?.trim()
if (!bundledConfigPath) {
await mkdir(configDirectory, { recursive: true, mode: 0o700 })
return
}
const target = resolve(configDirectory)
const existing = sharedConfigPreparations.get(target)
if (existing) {
return existing
}
const preparation = (async () => {
const source = resolve(bundledConfigPath)
const markerName = '.goodbuddy-ready.json'
const sourceMarker = await readFile(join(source, markerName), 'utf8')
try {
if (
(await readFile(join(target, markerName), 'utf8')) ===
sourceMarker
) {
return
}
} catch {
// Install the bundled dependency tree below.
}
await mkdir(dirname(target), { recursive: true, mode: 0o700 })
const staging = `${target}.staging-${process.pid}-${randomBytes(6).toString('hex')}`
await rm(staging, { recursive: true, force: true })
try {
await cp(source, staging, {
recursive: true,
errorOnExist: true,
force: false
})
await rm(target, { recursive: true, force: true })
await rename(staging, target)
} finally {
await rm(staging, { recursive: true, force: true })
}
})()
sharedConfigPreparations.set(target, preparation)
try {
await preparation
} catch (error) {
sharedConfigPreparations.delete(target)
throw error
}
}
private async inspectEmbeddedConfiguration(
validatePath = false
): Promise<{
@@ -957,12 +1097,17 @@ export class OpenCodeRuntime implements AgentRuntime {
env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = '1'
env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = '1'
env.OPENCODE_DISABLE_EXTERNAL_SKILLS = '1'
env.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER = '1'
env.OPENCODE_DISABLE_LSP_DOWNLOAD = '1'
env.OPENCODE_DISABLE_MODELS_FETCH = '1'
env.OPENCODE_DISABLE_PROJECT_CONFIG = '1'
env.OPENCODE_DISABLE_SHARE = '1'
env.XDG_CACHE_HOME = join(registration.root, 'xdg-cache')
env.XDG_CONFIG_HOME = join(registration.root, 'xdg-config')
env.XDG_CACHE_HOME = this.options.sharedCacheRoot?.trim()
? join(resolve(this.options.sharedCacheRoot), 'cache')
: join(registration.root, 'xdg-cache')
env.XDG_CONFIG_HOME = this.options.sharedCacheRoot?.trim()
? dirname(registration.configDirectory)
: join(registration.root, 'xdg-config')
env.XDG_DATA_HOME = join(registration.root, 'xdg-data')
env.XDG_STATE_HOME = join(registration.root, 'xdg-state')
const skillConfig = createOpenCodeSkillConfig(