feat(simulation): add named drive instances (#35648)

This commit is contained in:
James Long 2026-07-06 22:52:10 -04:00 committed by GitHub
commit fcb1d4b418
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 199 additions and 173 deletions

View file

@ -22,9 +22,6 @@ import { SimulationNetwork } from "./network"
* - `network.log` simulated network request log
*/
const DefaultPort = 40950
const MaxPortAttempts = 100
type ControlSocket = Bun.ServerWebSocket<{ unsubscribe?: () => void }>
function parseRequest(input: string | Buffer) {
@ -68,46 +65,35 @@ async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc
throw new Error(`Unknown simulation control method: ${request.method}`)
}
function serve(port = DefaultPort, attempts = MaxPortAttempts): Bun.Server<{ unsubscribe?: () => void }> {
try {
return Bun.serve<{ unsubscribe?: () => void }>({
hostname: "127.0.0.1",
port,
fetch(request, server) {
if (server.upgrade(request, { data: {} })) return undefined
return new Response("opencode simulation control websocket", { status: 426 })
export function start(endpoint: string) {
const url = new URL(endpoint)
const server = Bun.serve<{ unsubscribe?: () => void }>({
hostname: url.hostname,
port: Number(url.port),
fetch(request, server) {
if (server.upgrade(request, { data: {} })) return undefined
return new Response("opencode drive backend websocket", { status: 426 })
},
websocket: {
close(socket) {
socket.data.unsubscribe?.()
},
websocket: {
close(socket) {
socket.data.unsubscribe?.()
},
async message(socket, message) {
let request: SimulationProtocol.JsonRpc.Request | undefined
try {
request = parseRequest(message)
const result = await handle(socket, request)
const response = SimulationProtocol.JsonRpc.success(request.id, result)
if (response) socket.send(JSON.stringify(response))
} catch (error) {
socket.send(JSON.stringify(SimulationProtocol.JsonRpc.failure(request?.id, error)))
}
},
async message(socket, message) {
let request: SimulationProtocol.JsonRpc.Request | undefined
try {
request = parseRequest(message)
const result = await handle(socket, request)
const response = SimulationProtocol.JsonRpc.success(request.id, result)
if (response) socket.send(JSON.stringify(response))
} catch (error) {
socket.send(JSON.stringify(SimulationProtocol.JsonRpc.failure(request?.id, error)))
}
},
})
} catch (error) {
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()
const unavailable = message.includes("eaddrinuse") || message.includes("in use")
if (!unavailable || attempts <= 1 || port >= 65535) throw error
return serve(port + 1, attempts - 1)
}
}
export function start() {
const server = serve()
const url = `ws://${server.hostname}:${server.port}`
process.stderr.write(`opencode simulation backend control websocket: ${url}\n`)
},
})
process.stderr.write(`opencode drive backend websocket: ${endpoint}\n`)
return {
url,
url: endpoint,
stop: () => {
server.stop(true)
},

View file

@ -328,31 +328,31 @@ export function make(options: Options): FileSystem.FileSystem {
* Lazily constructed layer so the root defaults to `process.cwd()` at
* layer-build time (the simulation anchor directory), not at import time.
*
* When `OPENCODE_SIMULATION_STATE` points at a snapshot directory, its
* `project/` contents are read from the host once at build time and seeded
* When `OPENCODE_SIMULATE_STATE` points at a snapshot directory, its
* `files/` contents are read from the host once at build time and seeded
* into the in-memory tree, joined onto the anchor root.
*/
export const layer = (options?: Partial<Options>) =>
Layer.sync(FileSystem.FileSystem)(() =>
make({
root: options?.root ?? process.cwd(),
files: { ...loadSnapshotFiles(process.env.OPENCODE_SIMULATION_STATE), ...options?.files },
files: { ...loadSnapshotFiles(process.env.OPENCODE_SIMULATE_STATE), ...options?.files },
}),
)
function loadSnapshotFiles(stateDirectory: string | undefined) {
if (!stateDirectory) return {}
const project = path.join(stateDirectory, "project")
if (!nodeFs.existsSync(project)) return {}
const snapshot = path.join(stateDirectory, "files")
if (!nodeFs.existsSync(snapshot)) return {}
const files: Record<string, Uint8Array> = {}
const walk = (dir: string) => {
for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
const file = path.join(dir, entry.name)
if (entry.isDirectory()) walk(file)
if (entry.isFile()) files[path.relative(project, file)] = new Uint8Array(nodeFs.readFileSync(file))
if (entry.isFile()) files[path.relative(snapshot, file)] = new Uint8Array(nodeFs.readFileSync(file))
}
}
walk(project)
walk(snapshot)
return files
}

View file

@ -1,6 +1,7 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { filesystem, httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { DriveManifest } from "../manifest"
import { SimulationControl } from "./control"
import { SimulationFileSystem } from "./filesystem"
import { SimulationFSUtil } from "./fs-util"
@ -10,19 +11,15 @@ import { SimulationOpenAI } from "./openai"
/**
* Layer replacements applied when the server is built in simulation mode.
*
* The server merges these into the app node build when `OPENCODE_SIMULATION`
* The server merges these into the app node build when `OPENCODE_SIMULATE`
* is enabled, via a dynamic import so this module is never loaded eagerly.
*
* - Filesystem: in-memory tree rooted at `OPENCODE_SIMULATION_ROOT` (the real,
* empty anchor directory the runner created and chdir'd into). Everything
* under the root lives in memory; paths outside it fail loudly.
* - Filesystem: in-memory tree rooted at the current working directory.
* Everything under the root lives in memory; paths outside it fail loudly.
* - Network: all outbound HTTP resolves against the simulated route table;
* unknown destinations are denied. The driver-answered OpenAI endpoint is
* registered here as the first route.
*
* Loading this module also starts the backend simulation control WebSocket,
* which drivers connect to directly for LLM exchange control and network
* inspection (standalone topology; also the headless-simulation interface).
*/
SimulationNetwork.register(SimulationOpenAI.route)
@ -30,10 +27,12 @@ SimulationNetwork.register(SimulationOpenAI.route)
// an empty catalog; providers come from seeded config instead.
SimulationNetwork.register(SimulationNetwork.json("GET", "https://models.dev/api.json", {}))
SimulationControl.start()
export function startDriveServer() {
return SimulationControl.start(DriveManifest.resolve().endpoints.backend)
}
export const simulationReplacements: LayerNode.Replacements = [
[filesystem, SimulationFileSystem.layer({ root: process.env.OPENCODE_SIMULATION_ROOT })],
[filesystem, SimulationFileSystem.layer()],
[FSUtil.node, SimulationFSUtil.node],
[httpClient, SimulationNetwork.layer],
]