import { runtimePrivacyEnvironment } from "../shared/runtime-privacy-environment"; import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES, remoteRuntimeBundleManifestSchema, type RemoteRuntimeBundleManifest, } from "../shared/remote-runtime-launch-contracts"; import { modelBridgePolicySchema, type ModelBridgePolicy, } from "../shared/model-bridge-contracts"; import { assertAbsoluteManagedPath } from "./managed-paths"; const OPEN_CODE_RUNTIME_ENVIRONMENT = Object.freeze({ OPENCODE_DISABLE_AUTOUPDATE: "1", OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "1", OPENCODE_DISABLE_EMBEDDED_WEB_UI: "1", OPENCODE_DISABLE_EXTERNAL_SKILLS: "1", OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "1", OPENCODE_DISABLE_LSP_DOWNLOAD: "1", OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_PROJECT_CONFIG: "1", OPENCODE_DISABLE_SHARE: "1", }); export type OpenCodeLaunchProfile = { executable: string; processExecutable: string; args: readonly string[]; cwd: string; env: Readonly; }; export function createOpenCodeLaunchProfile(input: { manifest: RemoteRuntimeBundleManifest; bundleDirectory: string; workspaceDirectory: string; modelBridge?: { agentExecutablePath: string; bridgeDirectory: string; socketPath: string; policy: ModelBridgePolicy; sharedSessions?: boolean; }; }): OpenCodeLaunchProfile { const manifest = remoteRuntimeBundleManifestSchema.parse(input.manifest); const isContinue = manifest.runtimeId === "continue" && manifest.provider === "continue"; if ( !isContinue && (manifest.runtimeId !== "opencode" || manifest.provider !== "opencode" || manifest.entrypoint.identity !== "opencode-acp" || manifest.entrypoint.argvPrefix.length !== 1 || manifest.entrypoint.argvPrefix[0] !== "acp") ) { throw new Error("Runtime manifest is not the fixed OpenCode ACP profile"); } if (isContinue && (manifest.entrypoint.identity !== "continue-acp" || manifest.entrypoint.path !== "lib/continue/dist/cn.js" || manifest.entrypoint.argvPrefix.length !== 0 || !input.modelBridge)) { throw new Error("Continue requires its managed ACP facade and model bridge"); } if ( [...manifest.allowedEnvironmentNames].sort().join("\0") !== [...OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES].sort().join("\0") ) { throw new Error( "OpenCode Runtime environment allowlist does not match the fixed profile", ); } const bundleDirectory = assertAbsoluteManagedPath( resolve(input.bundleDirectory), ); const workspaceDirectory = normalizedAbsolutePath( input.workspaceDirectory, "Workspace", ); const modelBridge = input.modelBridge === undefined ? undefined : { agentExecutablePath: normalizedAbsolutePath( input.modelBridge.agentExecutablePath, "Agent executable", ), agentInstallationDirectory: normalizedAbsolutePath( dirname(input.modelBridge.agentExecutablePath), "Agent installation", ), bridgeDirectory: normalizedAbsolutePath( input.modelBridge.bridgeDirectory, "Model bridge", ), socketPath: normalizedAbsolutePath( input.modelBridge.socketPath, "Model bridge socket", ), policy: modelBridgePolicySchema.parse(input.modelBridge.policy), sharedSessions: input.modelBridge.sharedSessions === true, }; if ( modelBridge !== undefined && (!isStrictDescendant(modelBridge.bridgeDirectory, modelBridge.socketPath) || !isStrictDescendant( modelBridge.agentInstallationDirectory, modelBridge.agentExecutablePath, )) ) { throw new Error( "Model bridge socket or Agent executable escapes its managed directory", ); } const executablePath = resolve( bundleDirectory, ...manifest.entrypoint.path.split("/"), ); if (!isStrictDescendant(bundleDirectory, executablePath)) { throw new Error("OpenCode entrypoint escapes its bundle directory"); } const runtimeCommand = modelBridge === undefined ? { executable: executablePath, args: [...manifest.entrypoint.argvPrefix], } : { executable: join(modelBridge.agentInstallationDirectory, "node"), args: [ join(modelBridge.agentInstallationDirectory, "lib", "agent.cjs"), "model-bridge-helper", "--socket-path", modelBridge.socketPath, "--protocol", modelBridge.policy.protocol, "--model", modelBridge.policy.model, "--supports-image-input", modelBridge.policy.supportsImageInput ? "true" : "false", ...(modelBridge.sharedSessions ? ["--shared-sessions", "true"] : []), ...(isContinue ? ["--runtime-id", "continue"] : []), isContinue ? "--continue-entrypoint" : "--opencode-entrypoint", executablePath, ], }; const environmentBase = { ...executeEnvironment(process.env, workspaceDirectory), ...runtimePrivacyEnvironment, ...(isContinue ? {} : { ...OPEN_CODE_RUNTIME_ENVIRONMENT, OPENCODE_CONFIG_DIR: join(bundleDirectory, "config", "opencode") }), }; const permission = "allow"; const environment = { ...environmentBase, ...(isContinue ? {} : { OPENCODE_CONFIG_CONTENT: JSON.stringify({ // Native snapshot summaries are not consumed by GoodBuddy. snapshot: false, permission, agent: { build: { permission } }, }) }), }; return { ...runtimeCommand, processExecutable: runtimeCommand.executable, cwd: workspaceDirectory, env: environment, }; } function normalizedAbsolutePath(value: string, label: string): string { if (value.includes("\0") || !isAbsolute(value) || resolve(value) !== value) { throw new Error(`${label} path must be a normalized absolute path`); } return value; } function executeEnvironment( source: Readonly, workspaceDirectory: string, ): Readonly { const home = usableEnvironmentValue(source.HOME) ? source.HOME! : workspaceDirectory; const defaults = { HOME: home, LANG: "C.UTF-8", LC_ALL: "C.UTF-8", PATH: "/usr/bin:/bin", TMPDIR: "/tmp", XDG_CACHE_HOME: join(home, ".cache"), XDG_CONFIG_HOME: join(home, ".config"), XDG_DATA_HOME: join(home, ".local", "share"), XDG_STATE_HOME: join(home, ".local", "state"), } as const; const environment: NodeJS.ProcessEnv = { ...source }; for (const [name, fallback] of Object.entries(defaults)) { if (!usableEnvironmentValue(environment[name])) { environment[name] = fallback; } } return environment; } function usableEnvironmentValue(value: string | undefined): value is string { return value !== undefined && value.length > 0 && !value.includes("\0"); } function isStrictDescendant(parent: string, candidate: string): boolean { const child = relative(parent, candidate); return ( child.length > 0 && child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child) ); }