@@ -47,9 +47,33 @@ export interface ComputerAgentServerOptions {
4747 */
4848 readonly defaultEnvs ?: Readonly < Record < string , string > > ;
4949 /**
50- * Substrate factory — if absent, every request boots a fresh LocalSubstrate.
51- * Override to swap in `BwrapSubstrate` for namespace-isolated agents, an
52- * E2B template, a long-lived VM, or a test mock. Any `Substrate` works.
50+ * Registry of substrate factories keyed by wire-side `runtime` name.
51+ * Clients pick which to use per-request via the `runtime` field in the
52+ * /run body. Built-ins shipped with examples:
53+ * - "local" → LocalSubstrate (process boundary, no security boundary)
54+ * - "bwrap" → BwrapSubstrate (Linux namespace isolation, recommended)
55+ * - "e2b" → E2BSubstrate (Firecracker VMs, external service)
56+ *
57+ * Register the ones your deployment supports:
58+ *
59+ * substrates: {
60+ * local: () => new LocalSubstrate(),
61+ * bwrap: () => new BwrapSubstrate({ extraRoBinds: [...] }),
62+ * }
63+ *
64+ * If only one is registered, it's used regardless of what the client asks for.
65+ * If both `substrates` and the legacy `substrate` are absent, requests use a
66+ * fresh LocalSubstrate per call (matches the v0 default).
67+ */
68+ readonly substrates ?: Readonly < Record < string , ( ) => Substrate > > ;
69+ /**
70+ * Default `runtime` when the request body omits it. Must be a key in
71+ * `substrates`. If unset, the first registered key wins.
72+ */
73+ readonly defaultRuntime ?: string ;
74+ /**
75+ * @deprecated Pass via `substrates: { default: () => ... }` and `defaultRuntime: "default"` instead.
76+ * Kept for backward compatibility with the singular-substrate v0 shape.
5377 */
5478 readonly substrate ?: ( ) => Substrate ;
5579 /**
@@ -62,6 +86,13 @@ export interface ComputerAgentServerOptions {
6286interface RunBody {
6387 source : IdentitySource | string ;
6488 harness : string ;
89+ /**
90+ * Which substrate to spawn the agent inside. Names come from the server's
91+ * `substrates` registry (e.g. "local", "bwrap", "e2b"). If omitted, the
92+ * server uses its `defaultRuntime`. Unknown values → 400 UNKNOWN_RUNTIME
93+ * with the list of available names.
94+ */
95+ runtime ?: string ;
6596 message : string | Array < { role : "user" ; content : string } > ;
6697 envs ?: Record < string , string > ;
6798 options ?: Record < string , unknown > ;
@@ -125,7 +156,13 @@ export class ComputerAgentServer {
125156
126157 private wire ( ) : void {
127158 this . app . get ( "/health" , ( c ) =>
128- c . json ( { ok : true , activeRuns : this . runs . size , max : this . opts . maxConcurrentRuns ?? 4 } ) ,
159+ c . json ( {
160+ ok : true ,
161+ activeRuns : this . runs . size ,
162+ max : this . opts . maxConcurrentRuns ?? 4 ,
163+ runtimes : this . availableRuntimes ( ) ,
164+ defaultRuntime : this . resolveDefaultRuntime ( ) ,
165+ } ) ,
129166 ) ;
130167
131168 this . app . post ( "/run" , async ( c ) => {
@@ -144,6 +181,15 @@ export class ComputerAgentServer {
144181 const validation = validateRunBody ( body ) ;
145182 if ( validation ) return c . json ( { error : validation } , 400 ) ;
146183
184+ // Resolve which substrate factory to use for this request.
185+ // Precedence: body.runtime → defaultRuntime → first registered → legacy
186+ // singular `substrate` → fresh LocalSubstrate.
187+ const runtimeResult = this . resolveRuntime ( body . runtime ) ;
188+ if ( ! runtimeResult . ok ) {
189+ return c . json ( { error : runtimeResult . error } , 400 ) ;
190+ }
191+ const buildSubstrate = runtimeResult . factory ;
192+
147193 const source = applyGitToken ( normalizeSource ( body . source ) , body . gitToken ) ;
148194 const envs = {
149195 ...this . opts . defaultEnvs ,
@@ -158,7 +204,7 @@ export class ComputerAgentServer {
158204 const agent = new ComputerAgent ( {
159205 source,
160206 harness : body . harness as never ,
161- runtime : this . opts . substrate ? this . opts . substrate ( ) : new LocalSubstrate ( ) ,
207+ runtime : buildSubstrate ( ) ,
162208 envs,
163209 ...( body . options ? { options : body . options } : { } ) ,
164210 ...( body . model ? { model : body . model } : { } ) ,
@@ -252,6 +298,61 @@ export class ComputerAgentServer {
252298 return c . json ( { entries : tree } ) ;
253299 } ) ;
254300 }
301+
302+ /**
303+ * Map a per-request `runtime` name to the registered substrate factory.
304+ * Returns a discriminated result so the route handler can convert "unknown
305+ * runtime" into a clean 400 with the available list.
306+ */
307+ private resolveRuntime (
308+ requested : string | undefined ,
309+ ) :
310+ | { ok : true ; factory : ( ) => Substrate }
311+ | { ok : false ; error : { code : string ; message : string ; available : string [ ] } } {
312+ const registry = this . opts . substrates ;
313+ const fallback = this . opts . substrate ;
314+ const available = this . availableRuntimes ( ) ;
315+
316+ // Explicit request: must match a registered key.
317+ if ( requested ) {
318+ const factory = registry ?. [ requested ] ;
319+ if ( factory ) return { ok : true , factory } ;
320+ // If only the legacy singular substrate is set, accept whatever the
321+ // client asked for (one-substrate deployment — the name is informational).
322+ if ( ! registry && fallback ) return { ok : true , factory : fallback } ;
323+ return {
324+ ok : false ,
325+ error : {
326+ code : "UNKNOWN_RUNTIME" ,
327+ message : `runtime '${ requested } ' not registered on this server` ,
328+ available,
329+ } ,
330+ } ;
331+ }
332+
333+ // No explicit request: use defaultRuntime, then the first registered, then
334+ // the legacy singular factory, then a fresh LocalSubstrate.
335+ if ( registry ) {
336+ const defaultName = this . opts . defaultRuntime ?? Object . keys ( registry ) [ 0 ] ;
337+ const factory = defaultName ? registry [ defaultName ] : undefined ;
338+ if ( factory ) return { ok : true , factory } ;
339+ }
340+ if ( fallback ) return { ok : true , factory : fallback } ;
341+ return { ok : true , factory : ( ) => new LocalSubstrate ( ) } ;
342+ }
343+
344+ private availableRuntimes ( ) : string [ ] {
345+ if ( this . opts . substrates ) return Object . keys ( this . opts . substrates ) ;
346+ if ( this . opts . substrate ) return [ "<legacy-singleton>" ] ;
347+ return [ "local" ] ;
348+ }
349+
350+ private resolveDefaultRuntime ( ) : string | undefined {
351+ if ( this . opts . substrates ) {
352+ return this . opts . defaultRuntime ?? Object . keys ( this . opts . substrates ) [ 0 ] ;
353+ }
354+ return undefined ;
355+ }
255356}
256357
257358// ── helpers ──────────────────────────────────────────────────────────────
@@ -317,13 +418,22 @@ if (import.meta.url === `file://${process.argv[1]}`) {
317418 ? { ANTHROPIC_API_KEY : process . env . ANTHROPIC_API_KEY }
318419 : undefined ,
319420 maxConcurrentRuns : 4 ,
421+ // Register every substrate the deployment supports. Clients pick which
422+ // via `"runtime": "local" | "bwrap" | ...` in the /run body.
423+ substrates : {
424+ local : ( ) => new LocalSubstrate ( ) ,
425+ // Add others as needed:
426+ // bwrap: () => new BwrapSubstrate({ extraRoBinds: [...] }),
427+ // e2b: () => new E2BSubstrate({ apiKey: process.env.E2B_API_KEY! }),
428+ } ,
429+ defaultRuntime : "local" ,
320430 } ) ;
321431 const { host, port } = await server . listen ( ) ;
322432 console . log ( `ComputerAgentServer listening on http://${ host } :${ port } ` ) ;
323433 console . log ( "" ) ;
324434 console . log ( "Endpoints:" ) ;
325- console . log ( " GET /health" ) ;
326- console . log ( " POST /run body: {source, harness, message, envs?, options?, gitToken?, model?, debug?}" ) ;
435+ console . log ( " GET /health runtimes + default + active count " ) ;
436+ console . log ( " POST /run body: {source, harness, runtime?, message, envs?, options?, gitToken?, model?, sessionStore?, sessionId ?, debug?}" ) ;
327437 console . log ( " GET /workdir?sessionId=<id>" ) ;
328438 console . log ( " GET /artifact?sessionId=<id>&path=<path>" ) ;
329439 console . log ( "" ) ;
@@ -333,6 +443,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
333443 console . log ( " -d '{" ) ;
334444 console . log ( ' "source": "github.com/shreyas-lyzr/pdf-agent",' ) ;
335445 console . log ( ' "harness": "claude-agent-sdk",' ) ;
446+ console . log ( ' "runtime": "local",' ) ;
336447 console . log ( ' "options": { "permissionMode": "bypassPermissions", "settingSources": ["project"] },' ) ;
337448 console . log ( ' "message": "Write hello.pdf with one line of text"' ) ;
338449 console . log ( " }'" ) ;
0 commit comments