mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
94 lines
4.6 KiB
TypeScript
94 lines
4.6 KiB
TypeScript
// Guest-side handler for AGENT_HOOK_INSTALL_PLUGINS_METHOD: caches the plugin
|
|
// source the Windows host ships over the wire and materializes OpenCode's
|
|
// config overlay inside the guest. Extracted from the relay entrypoint so it is
|
|
// 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 { getRelayOpenCodePluginPath, type PluginOverlayManager } from './plugin-overlay'
|
|
import { resolveOpenCodeSourceConfigDir } from './plugin-overlay-env'
|
|
import { assertPluginSourceUnderByteCap } from './plugin-source-limit'
|
|
import {
|
|
sanitizeWslHookInstanceKey,
|
|
WSL_HOOK_RELAY_INSTANCE_ENV
|
|
} from '../shared/wsl-hook-relay-contract'
|
|
|
|
export type InstallPluginsResult = {
|
|
installed: { opencode: boolean; pi: boolean; omp: boolean; primeAgent: boolean }
|
|
overlayDirs: { opencode?: string }
|
|
}
|
|
|
|
export type InstallPluginsHandler = (params: Record<string, unknown>) => InstallPluginsResult
|
|
|
|
// 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
|
|
): InstallPluginsHandler {
|
|
// Why: materializeOpenCode wipes and rebuilds the overlay, and the id here is
|
|
// instance-scoped (not pane-scoped as on SSH). The host re-ships on every
|
|
// reinstall — 60s after connect and again on later pane spawns — so
|
|
// re-materializing unconditionally would delete the config root out from
|
|
// under running agents and race panes spawning against the path the host just
|
|
// handed them. Rebuild only when the shipped source actually changed.
|
|
let materialized: { source: string; sourceDir: string | undefined; dir: string } | null = null
|
|
|
|
return (params) => {
|
|
const opencode = params.opencodePluginSource
|
|
const pi = params.piExtensionSource
|
|
const omp = params.ompExtensionSource
|
|
const primeAgent = params.primeAgentExtensionSource
|
|
// Why: bound per-source bytes so a buggy/hostile host can't OOM the guest relay.
|
|
assertPluginSourceUnderByteCap('opencodePluginSource', opencode)
|
|
assertPluginSourceUnderByteCap('piExtensionSource', pi)
|
|
assertPluginSourceUnderByteCap('ompExtensionSource', omp)
|
|
assertPluginSourceUnderByteCap('primeAgentExtensionSource', primeAgent)
|
|
pluginOverlay.setSources({
|
|
opencodePluginSource: typeof opencode === 'string' ? opencode : undefined,
|
|
piExtensionSource: typeof pi === 'string' ? pi : undefined,
|
|
ompExtensionSource: typeof omp === 'string' ? omp : undefined,
|
|
primeAgentExtensionSource: typeof primeAgent === 'string' ? primeAgent : undefined
|
|
})
|
|
let opencodeDir: string | undefined
|
|
if (pluginOverlay.hasOpenCodeSource()) {
|
|
// An omitted source leaves the manager's cache untouched, so it counts as unchanged.
|
|
const incoming = typeof opencode === 'string' ? opencode : null
|
|
// Explicit-only (see header). Constant in practice for a relay's lifetime, so
|
|
// keying the cache on it is defensive; the rc scan behind it is memoized.
|
|
const sourceDir = resolveOpenCodeSourceConfigDir(env as Record<string, string>, env.SHELL)
|
|
const cached = materialized
|
|
if (
|
|
cached &&
|
|
(incoming === null || incoming === cached.source) &&
|
|
sourceDir === cached.sourceDir &&
|
|
// Why: the dir surviving a failed rebuild proves nothing — the plugin does.
|
|
existsSync(getRelayOpenCodePluginPath(cached.dir))
|
|
) {
|
|
opencodeDir = cached.dir
|
|
} else {
|
|
const overlayId =
|
|
sanitizeWslHookInstanceKey(env[WSL_HOOK_RELAY_INSTANCE_ENV]) ?? 'wsl-opencode'
|
|
// Why: null on write failure — caller falls back to the guest's own config (no status), never crossing a Windows overlay into WSL.
|
|
opencodeDir = pluginOverlay.materializeOpenCode(overlayId, sourceDir) ?? undefined
|
|
materialized =
|
|
opencodeDir && incoming !== null
|
|
? { source: incoming, sourceDir, dir: opencodeDir }
|
|
: null
|
|
}
|
|
}
|
|
return {
|
|
installed: {
|
|
opencode: pluginOverlay.hasOpenCodeSource(),
|
|
pi: pluginOverlay.hasPiSource('pi'),
|
|
omp: pluginOverlay.hasPiSource('omp'),
|
|
primeAgent: pluginOverlay.hasPiSource('prime-agent')
|
|
},
|
|
overlayDirs: opencodeDir ? { opencode: opencodeDir } : {}
|
|
}
|
|
}
|
|
}
|