-
Notifications
You must be signed in to change notification settings - Fork 0
Add WSL OpenCode runtime bootstrap #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
f76bb54
feat(contracts): add runtime bootstrap websocket contracts
098b4f4
feat(contracts): expose runtime bootstrap rpc contracts
e535a79
feat(web): bridge runtime bootstrap native api calls
e3a2896
feat(server): add wsl opencode runtime bootstrap
8158c31
feat(server): wire runtime bootstrap rpc handlers
e49ec1d
feat(web): add opencode runtime bootstrap controls
5392a36
docs: record wsl opencode bootstrap design
f92a762
docs: document local opencode runtime operations
ea4fffa
docs: clarify provider runtime bootstrap architecture
9630254
fix(server): address opencode bootstrap review feedback
9d1672f
fix(contracts): relax runtime bootstrap service names
45e63a6
docs: align wsl opencode helper paths
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
518 changes: 518 additions & 0 deletions
518
apps/server/src/provider/openCodeRuntimeBootstrap.test.ts
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,390 @@ | ||
| import type { | ||
| OpenCodeRuntimeProfile, | ||
| ProviderRuntimeBootstrapInput, | ||
| ProviderRuntimeBootstrapSnapshot, | ||
| } from "@jcode/contracts"; | ||
|
|
||
| export const JCODE_OPENCODE_SERVICE_NAME = "jcode-opencode.service" as const; | ||
| export const WSL_OPENCODE_PROFILE_ID = "wsl-opencode-service" as const; | ||
| export const WSL_OPENCODE_PROFILE_LABEL = "WSL OpenCode service" as const; | ||
| export const DEFAULT_WSL_OPENCODE_SERVER_URL = "http://127.0.0.1:4096" as const; | ||
|
|
||
| const WSL_OPENCODE_START_SCRIPT_NAME = "jcode-opencode-start" as const; | ||
|
|
||
| export interface WslOpenCodeBootstrapProbe { | ||
| readonly now: string; | ||
| readonly platform: string; | ||
| readonly osRelease: string; | ||
| readonly env: Readonly<Record<string, string | undefined>>; | ||
| readonly userSystemdAvailable: boolean; | ||
| readonly serviceExists: boolean; | ||
| readonly serviceActive: boolean; | ||
| readonly binaryPath: string | null; | ||
| readonly portAvailable: boolean; | ||
| readonly profileReachable: boolean; | ||
| readonly serverUrl?: string; | ||
| } | ||
|
|
||
| export interface WslOpenCodeBootstrapAdapter { | ||
| readonly paths?: WslOpenCodeBootstrapPaths; | ||
| readonly now: () => string; | ||
| readonly getProbe: () => Promise<WslOpenCodeBootstrapProbe>; | ||
| readonly ensureRuntimeDirectory: () => Promise<void>; | ||
| readonly resolveOpenCodeBinary: (forceReinstall: boolean) => Promise<string>; | ||
| readonly writeExecutableFile: (path: string, contents: string) => Promise<void>; | ||
| readonly writeFile: (path: string, contents: string) => Promise<void>; | ||
| readonly systemctlUser: (args: readonly string[]) => Promise<void>; | ||
| readonly smokeRuntime: (serverUrl: string) => Promise<void>; | ||
| } | ||
|
|
||
| export interface WslOpenCodeBootstrapPaths { | ||
| readonly runtimeDir: string; | ||
| readonly startScriptPath: string; | ||
| readonly serviceUnitPath: string; | ||
| readonly serverUrl: string; | ||
| readonly host: "127.0.0.1"; | ||
| readonly port: 4096; | ||
| } | ||
|
|
||
| type BootstrapState = ProviderRuntimeBootstrapSnapshot["state"]; | ||
|
|
||
| function isTruthyEnvMarker(value: string | undefined): boolean { | ||
| return Boolean(value?.trim()); | ||
| } | ||
|
|
||
| function isWslProbe(input: WslOpenCodeBootstrapProbe): boolean { | ||
| if (input.platform !== "linux") { | ||
| return false; | ||
| } | ||
|
|
||
| const osRelease = input.osRelease.toLowerCase(); | ||
| return ( | ||
| osRelease.includes("microsoft") || | ||
| osRelease.includes("wsl") || | ||
| isTruthyEnvMarker(input.env.WSL_DISTRO_NAME) || | ||
| isTruthyEnvMarker(input.env.WSL_INTEROP) | ||
| ); | ||
| } | ||
|
|
||
| function snapshot(input: { | ||
| readonly checkedAt: string; | ||
| readonly state: BootstrapState; | ||
| readonly message?: string; | ||
| readonly serviceName?: typeof JCODE_OPENCODE_SERVICE_NAME; | ||
| readonly binaryPath?: string; | ||
| readonly serverUrl?: string; | ||
| readonly profileId?: string; | ||
| }): ProviderRuntimeBootstrapSnapshot { | ||
| return { | ||
| provider: "opencode", | ||
| lane: "wsl-service", | ||
| state: input.state, | ||
| checkedAt: input.checkedAt, | ||
| ...(input.serviceName ? { serviceName: input.serviceName } : {}), | ||
| ...(input.binaryPath ? { binaryPath: input.binaryPath } : {}), | ||
| ...(input.serverUrl ? { serverUrl: input.serverUrl } : {}), | ||
| ...(input.profileId ? { profileId: input.profileId } : {}), | ||
| ...(input.message ? { message: redactBootstrapMessage(input.message) } : {}), | ||
| }; | ||
| } | ||
|
|
||
| export function detectWslOpenCodeBootstrapStatus( | ||
| input: WslOpenCodeBootstrapProbe, | ||
| ): ProviderRuntimeBootstrapSnapshot { | ||
| const serverUrl = input.serverUrl ?? DEFAULT_WSL_OPENCODE_SERVER_URL; | ||
|
|
||
| if (!isWslProbe(input)) { | ||
| return snapshot({ | ||
| checkedAt: input.now, | ||
| state: "unsupported", | ||
| message: "WSL OpenCode bootstrap is only supported inside WSL.", | ||
| }); | ||
| } | ||
|
|
||
| if (!input.userSystemdAvailable) { | ||
| return snapshot({ | ||
| checkedAt: input.now, | ||
| state: "unsupported", | ||
| message: "WSL user systemd is required for jcode-opencode.service.", | ||
| }); | ||
| } | ||
|
|
||
| if (!input.portAvailable) { | ||
| return snapshot({ | ||
| checkedAt: input.now, | ||
| state: "error", | ||
| serviceName: JCODE_OPENCODE_SERVICE_NAME, | ||
| serverUrl, | ||
| profileId: WSL_OPENCODE_PROFILE_ID, | ||
| ...(input.binaryPath ? { binaryPath: input.binaryPath } : {}), | ||
| message: "OpenCode loopback port 4096 is already occupied.", | ||
| }); | ||
| } | ||
|
|
||
| if (input.profileReachable) { | ||
| return snapshot({ | ||
| checkedAt: input.now, | ||
| state: "ready", | ||
| serverUrl, | ||
| profileId: WSL_OPENCODE_PROFILE_ID, | ||
| ...(input.binaryPath ? { binaryPath: input.binaryPath } : {}), | ||
| }); | ||
| } | ||
|
|
||
| if (!input.serviceExists || !input.binaryPath) { | ||
| return snapshot({ | ||
| checkedAt: input.now, | ||
| state: "notInstalled", | ||
| serverUrl, | ||
| profileId: WSL_OPENCODE_PROFILE_ID, | ||
| ...(input.binaryPath ? { binaryPath: input.binaryPath } : {}), | ||
| message: "WSL OpenCode service or binary is not installed.", | ||
| }); | ||
| } | ||
|
|
||
| if (!input.serviceActive) { | ||
| return snapshot({ | ||
| checkedAt: input.now, | ||
| state: "error", | ||
| serviceName: JCODE_OPENCODE_SERVICE_NAME, | ||
| binaryPath: input.binaryPath, | ||
| serverUrl, | ||
| profileId: WSL_OPENCODE_PROFILE_ID, | ||
| message: `${JCODE_OPENCODE_SERVICE_NAME} exists but is not active.`, | ||
| }); | ||
| } | ||
|
|
||
| return snapshot({ | ||
| checkedAt: input.now, | ||
| state: "error", | ||
| serviceName: JCODE_OPENCODE_SERVICE_NAME, | ||
| binaryPath: input.binaryPath, | ||
| serverUrl, | ||
| profileId: WSL_OPENCODE_PROFILE_ID, | ||
| message: `${JCODE_OPENCODE_SERVICE_NAME} is active but the OpenCode runtime is unreachable.`, | ||
| }); | ||
| } | ||
|
|
||
| export function renderJcodeOpenCodeServiceUnit(input: { | ||
| readonly startScriptPath: string; | ||
| }): string { | ||
| return [ | ||
| "[Unit]", | ||
| "Description=JCode external OpenCode runtime", | ||
| "After=network.target", | ||
| "", | ||
| "[Service]", | ||
| "Type=simple", | ||
| `ExecStart=${quoteSystemdExecArg(input.startScriptPath)}`, | ||
| "Restart=on-failure", | ||
| "RestartSec=2", | ||
| "", | ||
| "[Install]", | ||
| "WantedBy=default.target", | ||
| "", | ||
| ].join("\n"); | ||
| } | ||
|
|
||
| function quoteSystemdExecArg(value: string): string { | ||
| if (!/[\s"\\]/u.test(value)) { | ||
| return value; | ||
| } | ||
| return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"')}"`; | ||
| } | ||
|
|
||
| function quoteShellArg(value: string): string { | ||
| return `'${value.replace(/'/gu, "'\\''")}'`; | ||
| } | ||
|
|
||
| export function renderJcodeOpenCodeStartScript(input: { | ||
| readonly binaryPath: string; | ||
| readonly host: "127.0.0.1"; | ||
| readonly port: 4096; | ||
| }): string { | ||
| return [ | ||
| "#!/usr/bin/env sh", | ||
| "set -eu", | ||
| "unset OPENCODE_CONFIG_CONTENT", | ||
| `exec ${quoteShellArg(input.binaryPath)} serve --hostname=${input.host} --port=${input.port}`, | ||
| "", | ||
| ].join("\n"); | ||
| } | ||
|
|
||
| function withoutTrailingSlash(path: string): string { | ||
| return path.length > 1 ? path.replace(/\/+$/u, "") : path; | ||
| } | ||
|
|
||
| export function makeWslOpenCodeBootstrapPaths(input: { | ||
| readonly homeDir: string; | ||
| readonly runtimeDir: string; | ||
| readonly serverUrl?: string; | ||
| }): WslOpenCodeBootstrapPaths { | ||
| const homeDir = withoutTrailingSlash(input.homeDir); | ||
| const runtimeDir = withoutTrailingSlash(input.runtimeDir); | ||
|
|
||
| return { | ||
| runtimeDir, | ||
| startScriptPath: `${runtimeDir}/${WSL_OPENCODE_START_SCRIPT_NAME}`, | ||
| serviceUnitPath: `${homeDir}/.config/systemd/user/${JCODE_OPENCODE_SERVICE_NAME}`, | ||
| serverUrl: input.serverUrl ?? DEFAULT_WSL_OPENCODE_SERVER_URL, | ||
| host: "127.0.0.1", | ||
| port: 4096, | ||
| }; | ||
| } | ||
|
|
||
| export function makeWslOpenCodeRuntimeProfilePatch(input: { | ||
| readonly binaryPath: string; | ||
| readonly serverUrl?: string; | ||
| }): OpenCodeRuntimeProfile { | ||
| return { | ||
| id: WSL_OPENCODE_PROFILE_ID, | ||
| label: WSL_OPENCODE_PROFILE_LABEL, | ||
| provider: "opencode", | ||
| mode: "external", | ||
| configMode: "inherit", | ||
| serverUrl: input.serverUrl ?? DEFAULT_WSL_OPENCODE_SERVER_URL, | ||
| binaryPath: input.binaryPath, | ||
| skillRoots: [], | ||
| pluginRoots: [], | ||
| requiredCommands: [], | ||
| requiredSkills: [], | ||
| requiredPlugins: [], | ||
| requiredAgents: [], | ||
| requiredModels: [], | ||
| requiredEnv: [], | ||
| requirements: [], | ||
| capabilityPolicy: "warn", | ||
| }; | ||
| } | ||
|
|
||
| export function upsertWslOpenCodeRuntimeProfile( | ||
| existing: readonly OpenCodeRuntimeProfile[], | ||
| profile: OpenCodeRuntimeProfile, | ||
| ): OpenCodeRuntimeProfile[] { | ||
| const nextProfiles = existing.map((existingProfile) => | ||
| existingProfile.id === WSL_OPENCODE_PROFILE_ID ? profile : existingProfile, | ||
| ); | ||
|
|
||
| return existing.some((existingProfile) => existingProfile.id === WSL_OPENCODE_PROFILE_ID) | ||
| ? nextProfiles | ||
| : [...existing, profile]; | ||
| } | ||
|
|
||
| export function redactBootstrapMessage(message: string): string { | ||
| return message | ||
| .replace(/\b(client_secret)=([^\s&]+)/gi, "$1=<redacted>") | ||
| .replace(/\b(token|password)=([^\s&]+)/gi, "$1=<redacted>") | ||
| .replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/gi, "$1<redacted>@"); | ||
| } | ||
|
|
||
| export async function getWslOpenCodeRuntimeBootstrapStatus( | ||
| adapter: WslOpenCodeBootstrapAdapter, | ||
| ): Promise<ProviderRuntimeBootstrapSnapshot> { | ||
| return detectWslOpenCodeBootstrapStatus(await adapter.getProbe()); | ||
| } | ||
|
|
||
| export async function bootstrapWslOpenCodeRuntime( | ||
| adapter: WslOpenCodeBootstrapAdapter, | ||
| input: ProviderRuntimeBootstrapInput, | ||
| ): Promise<{ | ||
| readonly snapshot: ProviderRuntimeBootstrapSnapshot; | ||
| readonly profile: OpenCodeRuntimeProfile; | ||
| }> { | ||
| return runWslOpenCodeRuntimeOrchestration(adapter, input, { | ||
| serviceCommand: ["enable", "--now", JCODE_OPENCODE_SERVICE_NAME], | ||
| }); | ||
| } | ||
|
|
||
| export async function repairWslOpenCodeRuntime( | ||
| adapter: WslOpenCodeBootstrapAdapter, | ||
| input: ProviderRuntimeBootstrapInput, | ||
| ): Promise<{ | ||
| readonly snapshot: ProviderRuntimeBootstrapSnapshot; | ||
| readonly profile: OpenCodeRuntimeProfile; | ||
| }> { | ||
| return runWslOpenCodeRuntimeOrchestration(adapter, input, { | ||
| serviceCommand: ["restart", JCODE_OPENCODE_SERVICE_NAME], | ||
| }); | ||
| } | ||
|
|
||
| function getErrorMessage(cause: unknown): string { | ||
| if (cause instanceof Error) { | ||
| return cause.message; | ||
| } | ||
| if (typeof cause === "string") { | ||
| return cause; | ||
| } | ||
| return "WSL OpenCode runtime bootstrap failed."; | ||
| } | ||
|
|
||
| function dirname(path: string): string { | ||
| const index = path.lastIndexOf("/"); | ||
| return index > 0 ? path.slice(0, index) : "."; | ||
| } | ||
|
|
||
| function deriveWslHomeDirectory(binaryPath: string): string { | ||
| if (binaryPath.startsWith("/root/")) { | ||
| return "/root"; | ||
| } | ||
|
|
||
| const homeMatch = binaryPath.match(/^(\/home\/[^/]+)(?:\/|$)/); | ||
| if (homeMatch?.[1]) { | ||
| return homeMatch[1]; | ||
| } | ||
|
|
||
| const envHome = process.env.HOME?.trim(); | ||
| if (envHome?.startsWith("/")) { | ||
| return envHome; | ||
| } | ||
|
|
||
| throw new Error("Unable to resolve WSL home directory for OpenCode runtime bootstrap."); | ||
| } | ||
|
|
||
| function getFallbackBootstrapPaths(binaryPath: string): WslOpenCodeBootstrapPaths { | ||
| return makeWslOpenCodeBootstrapPaths({ | ||
| homeDir: deriveWslHomeDirectory(binaryPath), | ||
| runtimeDir: dirname(binaryPath), | ||
| }); | ||
| } | ||
|
|
||
| async function runWslOpenCodeRuntimeOrchestration( | ||
| adapter: WslOpenCodeBootstrapAdapter, | ||
| input: ProviderRuntimeBootstrapInput, | ||
| options: { readonly serviceCommand: readonly string[] }, | ||
| ): Promise<{ | ||
| readonly snapshot: ProviderRuntimeBootstrapSnapshot; | ||
| readonly profile: OpenCodeRuntimeProfile; | ||
| }> { | ||
| try { | ||
| await adapter.ensureRuntimeDirectory(); | ||
| const binaryPath = await adapter.resolveOpenCodeBinary(Boolean(input.forceReinstall)); | ||
| const paths = adapter.paths ?? getFallbackBootstrapPaths(binaryPath); | ||
| const profile = makeWslOpenCodeRuntimeProfilePatch({ binaryPath, serverUrl: paths.serverUrl }); | ||
|
|
||
| await adapter.writeExecutableFile( | ||
| paths.startScriptPath, | ||
| renderJcodeOpenCodeStartScript({ binaryPath, host: paths.host, port: paths.port }), | ||
| ); | ||
| await adapter.writeFile( | ||
| paths.serviceUnitPath, | ||
| renderJcodeOpenCodeServiceUnit({ startScriptPath: paths.startScriptPath }), | ||
| ); | ||
| await adapter.systemctlUser(["daemon-reload"]); | ||
| await adapter.systemctlUser(options.serviceCommand); | ||
| await adapter.smokeRuntime(paths.serverUrl); | ||
|
|
||
| return { | ||
| profile, | ||
| snapshot: snapshot({ | ||
| checkedAt: adapter.now(), | ||
| state: "ready", | ||
| serviceName: JCODE_OPENCODE_SERVICE_NAME, | ||
| binaryPath, | ||
| serverUrl: paths.serverUrl, | ||
| profileId: WSL_OPENCODE_PROFILE_ID, | ||
| }), | ||
| }; | ||
| } catch (cause) { | ||
| throw new Error(redactBootstrapMessage(getErrorMessage(cause))); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.