@agentorg/computer 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3751,7 +3751,6 @@ var require_websocket_server = __commonJS({
3751
3751
  });
3752
3752
 
3753
3753
  // src/main.ts
3754
- import { readFileSync as readFileSync6 } from "node:fs";
3755
3754
  import { homedir as homedir2 } from "node:os";
3756
3755
  import { isAbsolute as isAbsolute2, join as join11, resolve as resolve5 } from "node:path";
3757
3756
  import { parseArgs } from "node:util";
@@ -4829,10 +4828,10 @@ function $constructor(name, initializer4, proto, params) {
4829
4828
  if (initialized) {
4830
4829
  const own3 = Object.getPrototypeOf(inst);
4831
4830
  const ctorProto = inst._zod.constr.prototype;
4832
- let up = own3;
4833
- while (up && up !== ctorProto)
4834
- up = Object.getPrototypeOf(up);
4835
- const target = up ?? own3;
4831
+ let up2 = own3;
4832
+ while (up2 && up2 !== ctorProto)
4833
+ up2 = Object.getPrototypeOf(up2);
4834
+ const target = up2 ?? own3;
4836
4835
  if (!initialized.has(target)) {
4837
4836
  initialized.add(target);
4838
4837
  members(target, protoMembers);
@@ -18888,14 +18887,14 @@ var require_validate = /* @__PURE__ */ __commonJSMin(((exports) => {
18888
18887
  } else {
18889
18888
  const matches = RELATIVE_JSON_POINTER.exec($data);
18890
18889
  if (!matches) throw new Error(`Invalid JSON-pointer: ${$data}`);
18891
- const up = +matches[1];
18890
+ const up2 = +matches[1];
18892
18891
  jsonPointer = matches[2];
18893
18892
  if (jsonPointer === "#") {
18894
- if (up >= dataLevel) throw new Error(errorMsg("property/index", up));
18895
- return dataPathArr[dataLevel - up];
18893
+ if (up2 >= dataLevel) throw new Error(errorMsg("property/index", up2));
18894
+ return dataPathArr[dataLevel - up2];
18896
18895
  }
18897
- if (up > dataLevel) throw new Error(errorMsg("data", up));
18898
- data = dataNames[dataLevel - up];
18896
+ if (up2 > dataLevel) throw new Error(errorMsg("data", up2));
18897
+ data = dataNames[dataLevel - up2];
18899
18898
  if (!jsonPointer) return data;
18900
18899
  }
18901
18900
  let expr = data;
@@ -18905,8 +18904,8 @@ var require_validate = /* @__PURE__ */ __commonJSMin(((exports) => {
18905
18904
  expr = (0, codegen_1._)`${expr} && ${data}`;
18906
18905
  }
18907
18906
  return expr;
18908
- function errorMsg(pointerType, up) {
18909
- return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`;
18907
+ function errorMsg(pointerType, up2) {
18908
+ return `Cannot access ${pointerType} ${up2} levels up, current level is ${dataLevel}`;
18910
18909
  }
18911
18910
  }
18912
18911
  exports.getData = getData;
@@ -27958,7 +27957,10 @@ var StreamableHTTPClientTransport = class {
27958
27957
 
27959
27958
  // src/browser.ts
27960
27959
  var RUNS_CODE = "browser_run_code_unsafe";
27961
- var MAX_BODY = 8 * 1024 * 1024;
27960
+ var SCREENSHOT = "browser_take_screenshot";
27961
+ var MIB = 1024 * 1024;
27962
+ var MAX_BODY = 8 * MIB;
27963
+ var MAX_PICTURE = 3 * MIB;
27962
27964
  var START_MS = 3e4;
27963
27965
  var STOP_MS = 2e3;
27964
27966
  var ONE_REQUEST = "A body is one JSON-RPC request, as a JSON object: send each request in its own POST.";
@@ -27981,15 +27983,27 @@ function sendAnswer(upstream, response) {
27981
27983
  });
27982
27984
  }
27983
27985
  var self = fileURLToPath(import.meta.url);
27984
- function withoutHidden(text3, hidden) {
27985
- const clean = (json4) => {
27986
+ function changed(text3, change) {
27987
+ const one = (json4) => {
27986
27988
  const message = JSON.parse(json4);
27987
- if (message.result?.tools)
27988
- message.result.tools = message.result.tools.filter((tool) => !hidden.has(tool.name));
27989
+ change(message);
27989
27990
  return JSON.stringify(message);
27990
27991
  };
27991
- if (text3.trimStart().startsWith("{")) return clean(text3);
27992
- return text3.replace(/^data: (\{.*\})$/gm, (_, json4) => `data: ${clean(json4)}`);
27992
+ if (text3.trimStart().startsWith("{")) return one(text3);
27993
+ return text3.replace(/^data: (\{[^\n\r]*\})$/gm, (_, json4) => `data: ${one(json4)}`);
27994
+ }
27995
+ var withoutHidden = (hidden) => (message) => {
27996
+ if (message.result?.tools)
27997
+ message.result.tools = message.result.tools.filter((tool) => !hidden.has(tool.name));
27998
+ };
27999
+ function withoutLarge(message) {
28000
+ const sizes = (message.result?.content ?? []).map(
28001
+ (block) => block.type === "image" ? Buffer.byteLength(block.data ?? "", "base64") : 0
28002
+ );
28003
+ const size = Math.max(0, ...sizes);
28004
+ if (size <= MAX_PICTURE) return;
28005
+ const text3 = `The screenshot is ${(Math.ceil(size / MIB * 10) / 10).toFixed(1)} MiB, and the most a screenshot may be is ${MAX_PICTURE / MIB} MiB. Take it again without fullPage for what is on screen, or with element and target for one part of the page.`;
28006
+ message.result = { isError: true, content: [{ type: "text", text: text3 }] };
27993
28007
  }
27994
28008
  async function end(child) {
27995
28009
  if (child.exitCode !== null || child.signalCode !== null) return;
@@ -28152,11 +28166,12 @@ var Browser = class {
28152
28166
  headers,
28153
28167
  ...message ? { body: JSON.stringify(message) } : {}
28154
28168
  });
28155
- if (message?.method === "tools/list" && this.#hidden.size > 0) {
28156
- const text3 = withoutHidden(await upstream.text(), this.#hidden);
28157
- return sendAnswer(new Response(text3, upstream), response);
28158
- }
28159
- sendAnswer(upstream, response);
28169
+ let change;
28170
+ if (message?.method === "tools/list" && this.#hidden.size > 0)
28171
+ change = withoutHidden(this.#hidden);
28172
+ else if (tool === SCREENSHOT) change = withoutLarge;
28173
+ if (!change) return sendAnswer(upstream, response);
28174
+ sendAnswer(new Response(changed(await upstream.text(), change), upstream), response);
28160
28175
  }
28161
28176
  async stop() {
28162
28177
  this.#stopping = true;
@@ -30622,10 +30637,10 @@ function $constructor2(name, initializer32, proto, params) {
30622
30637
  if (initialized) {
30623
30638
  const own22 = Object.getPrototypeOf(inst);
30624
30639
  const ctorProto = inst._zod.constr.prototype;
30625
- let up = own22;
30626
- while (up && up !== ctorProto)
30627
- up = Object.getPrototypeOf(up);
30628
- const target = up ?? own22;
30640
+ let up2 = own22;
30641
+ while (up2 && up2 !== ctorProto)
30642
+ up2 = Object.getPrototypeOf(up2);
30643
+ const target = up2 ?? own22;
30629
30644
  if (!initialized.has(target)) {
30630
30645
  initialized.add(target);
30631
30646
  members2(target, protoMembers);
@@ -48482,21 +48497,21 @@ function visit(schema, fnOrHandlers) {
48482
48497
  case "object": {
48483
48498
  const oldShape = def.shape;
48484
48499
  const keys = Object.keys(oldShape);
48485
- let changed2 = false;
48500
+ let changed22 = false;
48486
48501
  const newShape = {};
48487
48502
  for (const k of keys) {
48488
48503
  const mapped = run3(oldShape[k]);
48489
48504
  if (mapped !== oldShape[k])
48490
- changed2 = true;
48505
+ changed22 = true;
48491
48506
  newShape[k] = mapped;
48492
48507
  }
48493
48508
  let newCatchall = def.catchall;
48494
48509
  if (def.catchall) {
48495
48510
  newCatchall = run3(def.catchall);
48496
48511
  if (newCatchall !== def.catchall)
48497
- changed2 = true;
48512
+ changed22 = true;
48498
48513
  }
48499
- return changed2 ? clone2(s, { ...def, shape: newShape, catchall: newCatchall }) : s;
48514
+ return changed22 ? clone2(s, { ...def, shape: newShape, catchall: newCatchall }) : s;
48500
48515
  }
48501
48516
  case "array": {
48502
48517
  const mapped = run3(def.element);
@@ -48504,21 +48519,21 @@ function visit(schema, fnOrHandlers) {
48504
48519
  }
48505
48520
  case "tuple": {
48506
48521
  const oldItems = def.items;
48507
- let changed2 = false;
48522
+ let changed22 = false;
48508
48523
  const newItems = [];
48509
48524
  for (const item of oldItems) {
48510
48525
  const mapped = run3(item);
48511
48526
  if (mapped !== item)
48512
- changed2 = true;
48527
+ changed22 = true;
48513
48528
  newItems.push(mapped);
48514
48529
  }
48515
48530
  let newRest = def.rest;
48516
48531
  if (def.rest) {
48517
48532
  newRest = run3(def.rest);
48518
48533
  if (newRest !== def.rest)
48519
- changed2 = true;
48534
+ changed22 = true;
48520
48535
  }
48521
- return changed2 ? clone2(s, { ...def, items: newItems, rest: newRest }) : s;
48536
+ return changed22 ? clone2(s, { ...def, items: newItems, rest: newRest }) : s;
48522
48537
  }
48523
48538
  case "record":
48524
48539
  case "map": {
@@ -48532,15 +48547,15 @@ function visit(schema, fnOrHandlers) {
48532
48547
  }
48533
48548
  case "union": {
48534
48549
  const oldOptions = def.options;
48535
- let changed2 = false;
48550
+ let changed22 = false;
48536
48551
  const newOptions = [];
48537
48552
  for (const opt of oldOptions) {
48538
48553
  const mapped = run3(opt);
48539
48554
  if (mapped !== opt)
48540
- changed2 = true;
48555
+ changed22 = true;
48541
48556
  newOptions.push(mapped);
48542
48557
  }
48543
- return changed2 ? clone2(s, { ...def, options: newOptions }) : s;
48558
+ return changed22 ? clone2(s, { ...def, options: newOptions }) : s;
48544
48559
  }
48545
48560
  case "intersection": {
48546
48561
  const newLeft = run3(def.left);
@@ -56621,14 +56636,14 @@ var require_validate2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
56621
56636
  } else {
56622
56637
  const matches = RELATIVE_JSON_POINTER.exec($data);
56623
56638
  if (!matches) throw new Error(`Invalid JSON-pointer: ${$data}`);
56624
- const up = +matches[1];
56639
+ const up2 = +matches[1];
56625
56640
  jsonPointer = matches[2];
56626
56641
  if (jsonPointer === "#") {
56627
- if (up >= dataLevel) throw new Error(errorMsg("property/index", up));
56628
- return dataPathArr[dataLevel - up];
56642
+ if (up2 >= dataLevel) throw new Error(errorMsg("property/index", up2));
56643
+ return dataPathArr[dataLevel - up2];
56629
56644
  }
56630
- if (up > dataLevel) throw new Error(errorMsg("data", up));
56631
- data = dataNames[dataLevel - up];
56645
+ if (up2 > dataLevel) throw new Error(errorMsg("data", up2));
56646
+ data = dataNames[dataLevel - up2];
56632
56647
  if (!jsonPointer) return data;
56633
56648
  }
56634
56649
  let expr = data;
@@ -56638,8 +56653,8 @@ var require_validate2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
56638
56653
  expr = (0, codegen_1._)`${expr} && ${data}`;
56639
56654
  }
56640
56655
  return expr;
56641
- function errorMsg(pointerType, up) {
56642
- return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`;
56656
+ function errorMsg(pointerType, up2) {
56657
+ return `Cannot access ${pointerType} ${up2} levels up, current level is ${dataLevel}`;
56643
56658
  }
56644
56659
  }
56645
56660
  exports.getData = getData;
@@ -65904,6 +65919,7 @@ var Sessions = class {
65904
65919
  this.#room();
65905
65920
  const started = Date.now();
65906
65921
  const session = this.#start(input2.cmd, input2.workdir ?? this.#root);
65922
+ if (input2.group) session.group = input2.group;
65907
65923
  const wait = clamp(input2.yield_time_ms ?? DEFAULT_YIELD_MS, MIN_YIELD_MS, MAX_YIELD_MS);
65908
65924
  return this.#after(session, wait, started, input2.max_output_tokens);
65909
65925
  }
@@ -65988,26 +66004,37 @@ var Sessions = class {
65988
66004
  if (!session) throw new ExecRefused(`No terminal ${id.slice(0, 64)} is here.`);
65989
66005
  await this.#end([session], "SIGINT");
65990
66006
  }
66007
+ /**
66008
+ * End the terminals still running of one group, as `stop` ends one, and say how many. A shell a
66009
+ * person opened is in no group.
66010
+ */
66011
+ async stopGroup(group) {
66012
+ const running2 = [...this.#sessions.values()].filter(
66013
+ (session) => session.group === group && session.exitCode === void 0
66014
+ );
66015
+ await this.#end(running2, "SIGINT");
66016
+ return running2.length;
66017
+ }
65991
66018
  /** Stop every terminal: a polite signal, then a certain one. */
65992
66019
  async stopAll() {
65993
66020
  await this.#end([...this.#sessions.values()], "SIGTERM");
65994
66021
  }
65995
66022
  async #end(sessions, polite) {
65996
- const running = sessions.filter((s) => s.exitCode === void 0);
65997
- for (const session of running) session.signal(session.tty ? "SIGHUP" : polite);
66023
+ const running2 = sessions.filter((s) => s.exitCode === void 0);
66024
+ for (const session of running2) session.signal(session.tty ? "SIGHUP" : polite);
65998
66025
  let timer;
65999
66026
  await Promise.race([
66000
- Promise.all(running.map((s) => s.ended)),
66027
+ Promise.all(running2.map((s) => s.ended)),
66001
66028
  new Promise((done) => {
66002
66029
  timer = setTimeout(done, KILL_AFTER_MS);
66003
66030
  })
66004
66031
  ]);
66005
66032
  clearTimeout(timer);
66006
- for (const session of running) if (session.exitCode === void 0) session.signal("SIGKILL");
66033
+ for (const session of running2) if (session.exitCode === void 0) session.signal("SIGKILL");
66007
66034
  }
66008
66035
  #room() {
66009
- const running = [...this.#sessions.values()].filter((s) => s.exitCode === void 0);
66010
- if (running.length >= MAX_RUNNING)
66036
+ const running2 = [...this.#sessions.values()].filter((s) => s.exitCode === void 0);
66037
+ if (running2.length >= MAX_RUNNING)
66011
66038
  throw new ExecRefused(`${MAX_RUNNING} commands are running. Wait for one, or stop one.`);
66012
66039
  }
66013
66040
  #running(id) {
@@ -66379,7 +66406,7 @@ function searched(job) {
66379
66406
  const more = out.length > limit;
66380
66407
  return (more ? [...out.slice(0, limit), `\u2026 more than ${limit} matches`] : out).join("\n");
66381
66408
  }
66382
- function changed(job) {
66409
+ function changed2(job) {
66383
66410
  const changes = [];
66384
66411
  let more = false;
66385
66412
  const newest = () => {
@@ -66432,7 +66459,7 @@ function changedSince(root, since) {
66432
66459
  }
66433
66460
  if (!isMainThread && workerData?.agentexecSearch) {
66434
66461
  const job = workerData;
66435
- parentPort?.postMessage("since" in job ? changed(job) : searched(job));
66462
+ parentPort?.postMessage("since" in job ? changed2(job) : searched(job));
66436
66463
  }
66437
66464
  var MAX_READ_BYTES = 1024 * 1024;
66438
66465
  function createHostFiles(root, level) {
@@ -66585,6 +66612,14 @@ var Answered = {
66585
66612
  session_id: external_exports.string().nullable().describe("For write_stdin, while it is still running."),
66586
66613
  text: external_exports.string().describe("What it wrote since the last call, cut in the middle if long.")
66587
66614
  };
66615
+ var GROUP = "agentexec/group";
66616
+ var STOP_GROUP = "agentexec/group/stop";
66617
+ var Group = external_exports.string().min(1).max(200);
66618
+ var groupOf = (meta3) => {
66619
+ const word = Group.optional().safeParse(meta3?.[GROUP]);
66620
+ if (!word.success) throw new ExecRefused(`${GROUP} in _meta is a string of 1 to 200 characters.`);
66621
+ return word.data;
66622
+ };
66588
66623
  var YieldTime = external_exports.number().int().positive().optional();
66589
66624
  var MaxTokens = external_exports.number().int().positive().optional();
66590
66625
  var READ_ONLY = { readOnlyHint: true };
@@ -66613,6 +66648,11 @@ function createServer2(root, sessions, version22, level, extraTools = []) {
66613
66648
  }
66614
66649
  );
66615
66650
  if (level === "terminal") {
66651
+ server.server.setRequestHandler(
66652
+ STOP_GROUP,
66653
+ { params: external_exports.object({ group: Group }), result: external_exports.object({ stopped: external_exports.number() }) },
66654
+ async ({ group }) => ({ stopped: await sessions.stopGroup(group) })
66655
+ );
66616
66656
  server.registerTool(
66617
66657
  "exec_command",
66618
66658
  {
@@ -66626,7 +66666,7 @@ function createServer2(root, sessions, version22, level, extraTools = []) {
66626
66666
  outputSchema: Answered,
66627
66667
  annotations: { destructiveHint: true }
66628
66668
  },
66629
- (input2) => answering(() => sessions.exec(input2))
66669
+ (input2, ctx) => answering(async () => sessions.exec({ ...input2, group: groupOf(ctx.mcpReq._meta) }))
66630
66670
  );
66631
66671
  server.registerTool(
66632
66672
  "write_stdin",
@@ -66729,7 +66769,7 @@ function createServer2(root, sessions, version22, level, extraTools = []) {
66729
66769
  );
66730
66770
  return server;
66731
66771
  }
66732
- var VERSION = "0.0.7";
66772
+ var VERSION = "0.0.8";
66733
66773
  function createAgentExec(options) {
66734
66774
  const root = realpathSync(resolve3(options.workspace));
66735
66775
  const level = options.level ?? "read";
@@ -67034,8 +67074,8 @@ var BrowserLive = class {
67034
67074
  }
67035
67075
  /** The cast of a page, started if no socket was shown it yet. */
67036
67076
  #cast(page) {
67037
- const running = this.#casts.get(page);
67038
- if (running) return running;
67077
+ const running2 = this.#casts.get(page);
67078
+ if (running2) return running2;
67039
67079
  const onFrame = (frame) => {
67040
67080
  const watching = this.#shownTo(page);
67041
67081
  if (frame.width !== cast.size.width || frame.height !== cast.size.height) {
@@ -67748,7 +67788,6 @@ var loadPty = () => {
67748
67788
  return pty;
67749
67789
  };
67750
67790
  var ProcessRuntime = class {
67751
- isolation = "none";
67752
67791
  #config;
67753
67792
  #contained;
67754
67793
  /** `contained` is for inside a container, where the computer and its load are its own. */
@@ -67756,6 +67795,9 @@ var ProcessRuntime = class {
67756
67795
  this.#config = config3;
67757
67796
  this.#contained = contained;
67758
67797
  }
67798
+ async available() {
67799
+ return true;
67800
+ }
67759
67801
  async prepare() {
67760
67802
  }
67761
67803
  async stopLeftover() {
@@ -67850,7 +67892,7 @@ async function serveInside(options) {
67850
67892
  if (!token) throw new Error(`${INNER_TOKEN_VARIABLE} is not set.`);
67851
67893
  delete process.env[INNER_TOKEN_VARIABLE];
67852
67894
  const digest = digestOf(token);
67853
- const running = new ProcessRuntime({ path: BROWSER, sandbox: false }, true).start(
67895
+ const running2 = new ProcessRuntime({ path: BROWSER, sandbox: false }, true).start(
67854
67896
  {
67855
67897
  id: "inside",
67856
67898
  ...options,
@@ -67877,13 +67919,13 @@ async function serveInside(options) {
67877
67919
  if (!given || !sameToken(given, digest)) return void response.writeHead(401).end();
67878
67920
  const { pathname } = new URL(request.url ?? "/", "http://localhost");
67879
67921
  if (pathname === "/ready") {
67880
- const { browserTools, surfaces } = await running;
67922
+ const { browserTools, surfaces } = await running2;
67881
67923
  response.writeHead(200, { "Content-Type": "application/json" });
67882
67924
  return void response.end(JSON.stringify({ browserTools, surfaces }));
67883
67925
  }
67884
67926
  const endpoint = /^\/mcp\/(computer|browser)$/.exec(pathname)?.[1];
67885
67927
  if (!endpoint) return void response.writeHead(404).end();
67886
- return (await running).handle(endpoint, request, response);
67928
+ return (await running2).handle(endpoint, request, response);
67887
67929
  };
67888
67930
  const server = createServer3((request, response) => {
67889
67931
  route(request, response).catch((error62) => {
@@ -67899,14 +67941,14 @@ async function serveInside(options) {
67899
67941
  const mode = url3.searchParams.get("mode");
67900
67942
  if (!given || !sameToken(given, digest) || !surface || mode !== "watch" && mode !== "operate")
67901
67943
  return void socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
67902
- running.then((computer) => computer.openLive(surface, mode, request, socket, head)).catch(() => socket.destroy());
67944
+ running2.then((computer) => computer.openLive(surface, mode, request, socket, head)).catch(() => socket.destroy());
67903
67945
  });
67904
67946
  server.listen(INNER_PORT, "0.0.0.0");
67905
67947
  for (const signal of ["SIGINT", "SIGTERM"])
67906
67948
  process.once(signal, () => {
67907
- void running.then((computer) => computer.stop()).finally(() => process.exit(0));
67949
+ void running2.then((computer) => computer.stop()).finally(() => process.exit(0));
67908
67950
  });
67909
- await running;
67951
+ await running2;
67910
67952
  }
67911
67953
 
67912
67954
  // src/container.ts
@@ -67941,17 +67983,30 @@ var UPGRADE_HEADERS = [
67941
67983
  var nameOf = (id) => `agentcomputer-${id}`;
67942
67984
  var sleep5 = (ms) => new Promise((done) => setTimeout(done, ms));
67943
67985
  var ContainerRuntime = class {
67944
- isolation = "container";
67945
67986
  #image;
67946
67987
  constructor(image) {
67947
67988
  this.#image = image;
67948
67989
  }
67949
- async prepare() {
67990
+ /** Whether Docker answers, asked for no longer than this. */
67991
+ async #answers(ms) {
67950
67992
  try {
67951
- await docker(["version", "--format", "{{.Server.Version}}"]);
67952
- } catch (error62) {
67953
- throw new Error(`Container isolation needs Docker, which didn't answer: ${String(error62)}`);
67993
+ await run2("docker", ["version", "--format", "{{.Server.Version}}"], { timeout: ms });
67994
+ return true;
67995
+ } catch {
67996
+ return false;
67954
67997
  }
67998
+ }
67999
+ // Asked each time the service says what it can make, so it doesn't wait on a Docker that hangs.
68000
+ available() {
68001
+ return this.#answers(1e3);
68002
+ }
68003
+ /** Docker, or the refusal of what can't be done without it. */
68004
+ async #docker() {
68005
+ if (!await this.#answers(1e4))
68006
+ throw new ApiError(409, "A container needs Docker, which isn't answering on this machine.");
68007
+ }
68008
+ async prepare() {
68009
+ await this.#docker();
67955
68010
  try {
67956
68011
  await docker(["image", "inspect", "--format", "{{.Id}}", this.#image]);
67957
68012
  } catch {
@@ -68092,11 +68147,14 @@ var ContainerRuntime = class {
68092
68147
  }
68093
68148
  throw new Error("Its container didn't start. Its logs say why.");
68094
68149
  }
68150
+ // Neither is said to be done where Docker doesn't answer: the container would be left as it is.
68095
68151
  async stopLeftover(id) {
68152
+ await this.#docker();
68096
68153
  await docker(["stop", "--time", STOP_SECONDS, nameOf(id)]).catch(() => {
68097
68154
  });
68098
68155
  }
68099
68156
  async remove(id) {
68157
+ await this.#docker();
68100
68158
  await docker(["rm", "--force", nameOf(id)]).catch(() => {
68101
68159
  });
68102
68160
  }
@@ -68145,10 +68203,14 @@ async function forward(url3, authorization, request, response) {
68145
68203
  }
68146
68204
 
68147
68205
  // src/handlers.ts
68148
- var VERSION2 = "0.0.14";
68206
+ var VERSION2 = "0.0.15";
68149
68207
  function createHandlers(computers) {
68150
68208
  return {
68151
- getInfo: async () => ({ version: VERSION2, isolation: computers.isolation }),
68209
+ getInfo: async () => ({
68210
+ version: VERSION2,
68211
+ isolation: computers.isolation,
68212
+ isolationsAvailable: await computers.available()
68213
+ }),
68152
68214
  listComputers: async ({ state }) => computers.list(state),
68153
68215
  createComputer: (input2) => computers.create(input2),
68154
68216
  getComputer: async ({ computerID }) => computers.get(computerID),
@@ -68167,6 +68229,81 @@ function createHandlers(computers) {
68167
68229
  };
68168
68230
  }
68169
68231
 
68232
+ // src/running.ts
68233
+ import { spawn as spawn4 } from "node:child_process";
68234
+ import { closeSync, openSync, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
68235
+ import { join as join9 } from "node:path";
68236
+ var MAX_LOG = 1024 * 1024;
68237
+ var STOP_MS2 = 3e4;
68238
+ var sleep6 = (ms) => new Promise((done) => setTimeout(done, ms));
68239
+ var recordOf = (data) => join9(data, "server.json");
68240
+ function recorded(data) {
68241
+ try {
68242
+ const { url: url3, pid } = JSON.parse(readFileSync4(recordOf(data), "utf8"));
68243
+ return { url: url3, pid, token: readFileSync4(join9(data, "control-token"), "utf8").trim() };
68244
+ } catch {
68245
+ return void 0;
68246
+ }
68247
+ }
68248
+ async function running(data, said = recorded(data)) {
68249
+ if (!said) return void 0;
68250
+ try {
68251
+ const answer = await fetch(`${said.url}/v1/info`, {
68252
+ headers: { Authorization: `Bearer ${said.token}` },
68253
+ signal: AbortSignal.timeout(3e3)
68254
+ });
68255
+ if (!answer.ok) return void 0;
68256
+ const { version: version3, isolation } = await answer.json();
68257
+ return { url: said.url, pid: said.pid, version: version3, isolation };
68258
+ } catch {
68259
+ return void 0;
68260
+ }
68261
+ }
68262
+ async function up(data, program, args) {
68263
+ const already = await running(data);
68264
+ if (already) return already;
68265
+ const log = join9(privateFolder(data), "service.log");
68266
+ try {
68267
+ const before = readFileSync4(log);
68268
+ if (before.length > MAX_LOG) writeFileSync2(log, before.subarray(before.length - MAX_LOG));
68269
+ } catch {
68270
+ }
68271
+ const from = statSync3(log, { throwIfNoEntry: false })?.size ?? 0;
68272
+ const out = openSync(log, "a", 384);
68273
+ const child = spawn4(process.execPath, [program, "serve", ...args], {
68274
+ detached: true,
68275
+ stdio: ["ignore", out, out]
68276
+ });
68277
+ closeSync(out);
68278
+ child.unref();
68279
+ let ended = false;
68280
+ child.once("exit", () => {
68281
+ ended = true;
68282
+ });
68283
+ for (; ; await sleep6(100)) {
68284
+ const said = recorded(data);
68285
+ const up2 = await running(data, said);
68286
+ const own3 = said?.pid === child.pid;
68287
+ if (up2 && (own3 || ended)) return up2;
68288
+ if (!ended && !own3) continue;
68289
+ if (!ended) {
68290
+ child.kill();
68291
+ throw new Error(`The service doesn't answer at ${said?.url} from here, so it was stopped.`);
68292
+ }
68293
+ const wrote = readFileSync4(log).subarray(from).toString().trim();
68294
+ throw new Error(`The service didn't start.${wrote ? ` It said:
68295
+ ${wrote}` : ""}`);
68296
+ }
68297
+ }
68298
+ async function down(data) {
68299
+ const found = await running(data);
68300
+ if (!found) return;
68301
+ process.kill(found.pid, "SIGTERM");
68302
+ for (const deadline = Date.now() + STOP_MS2; Date.now() < deadline; await sleep6(100))
68303
+ if (!await running(data)) return;
68304
+ throw new Error(`The service at ${found.url} didn't stop.`);
68305
+ }
68306
+
68170
68307
  // node_modules/openapi-fetch/dist/index.mjs
68171
68308
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
68172
68309
  var supportsRequestInitExt = () => {
@@ -68852,13 +68989,14 @@ var Computer = class _Computer {
68852
68989
  this.files = new Files(this.#computer);
68853
68990
  }
68854
68991
  static async create(opts) {
68855
- const { access, browser, workspace, name, timeoutMs, metadata, apiKey, apiUrl } = opts;
68992
+ const { access, browser, isolation, workspace, name, timeoutMs, metadata, apiKey, apiUrl } = opts;
68856
68993
  const connection = { ...apiKey ? { apiKey } : {}, ...apiUrl ? { apiUrl } : {} };
68857
68994
  const created = answered(
68858
68995
  await apiOf(connection).POST("/v1/computers", {
68859
68996
  body: {
68860
68997
  access,
68861
68998
  browser: browser ?? false,
68999
+ ...isolation === void 0 ? {} : { isolation },
68862
69000
  ...workspace === void 0 ? {} : { workspace },
68863
69001
  ...name === void 0 ? {} : { name },
68864
69002
  ...timeoutMs === void 0 ? {} : { timeout: seconds(timeoutMs) },
@@ -68963,13 +69101,12 @@ var Computer = class _Computer {
68963
69101
  };
68964
69102
 
68965
69103
  // src/service.ts
68966
- import { readFileSync as readFileSync5, rmSync as rmSync3 } from "node:fs";
69104
+ import { readFileSync as readFileSync6, rmSync as rmSync3 } from "node:fs";
68967
69105
  import {
68968
69106
  createServer as createServer5,
68969
69107
  STATUS_CODES as STATUS_CODES3
68970
69108
  } from "node:http";
68971
69109
  import { createServer as createTlsServer } from "node:https";
68972
- import { join as join10 } from "node:path";
68973
69110
 
68974
69111
  // node_modules/@modelcontextprotocol/server/dist/chunk-vYduTEsO.mjs
68975
69112
  var __create4 = Object.create;
@@ -72430,7 +72567,7 @@ var REQUEST_STATE_ONLY_LEG_PACING_MS3 = 250;
72430
72567
  function inputRequiredRoundsExceededMessage3(method, maxRounds) {
72431
72568
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
72432
72569
  }
72433
- function sleep6(ms, signal) {
72570
+ function sleep7(ms, signal) {
72434
72571
  return new Promise((resolve6, reject) => {
72435
72572
  if (signal?.aborted) {
72436
72573
  reject(signal.reason instanceof SdkError3 ? signal.reason : new SdkError3(SdkErrorCode3.RequestTimeout, String(signal.reason)));
@@ -76061,14 +76198,14 @@ var require_validate3 = /* @__PURE__ */ __commonJSMin3(((exports) => {
76061
76198
  } else {
76062
76199
  const matches = RELATIVE_JSON_POINTER.exec($data);
76063
76200
  if (!matches) throw new Error(`Invalid JSON-pointer: ${$data}`);
76064
- const up = +matches[1];
76201
+ const up2 = +matches[1];
76065
76202
  jsonPointer = matches[2];
76066
76203
  if (jsonPointer === "#") {
76067
- if (up >= dataLevel) throw new Error(errorMsg("property/index", up));
76068
- return dataPathArr[dataLevel - up];
76204
+ if (up2 >= dataLevel) throw new Error(errorMsg("property/index", up2));
76205
+ return dataPathArr[dataLevel - up2];
76069
76206
  }
76070
- if (up > dataLevel) throw new Error(errorMsg("data", up));
76071
- data = dataNames[dataLevel - up];
76207
+ if (up2 > dataLevel) throw new Error(errorMsg("data", up2));
76208
+ data = dataNames[dataLevel - up2];
76072
76209
  if (!jsonPointer) return data;
76073
76210
  }
76074
76211
  let expr = data;
@@ -76078,8 +76215,8 @@ var require_validate3 = /* @__PURE__ */ __commonJSMin3(((exports) => {
76078
76215
  expr = (0, codegen_1._)`${expr} && ${data}`;
76079
76216
  }
76080
76217
  return expr;
76081
- function errorMsg(pointerType, up) {
76082
- return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`;
76218
+ function errorMsg(pointerType, up2) {
76219
+ return `Cannot access ${pointerType} ${up2} levels up, current level is ${dataLevel}`;
76083
76220
  }
76084
76221
  }
76085
76222
  exports.getData = getData;
@@ -81427,7 +81564,7 @@ var LegacyInputRequiredShim2 = class {
81427
81564
  } finally {
81428
81565
  roundAbort.dispose();
81429
81566
  }
81430
- } else await sleep6(REQUEST_STATE_ONLY_LEG_PACING_MS3, outerSignal);
81567
+ } else await sleep7(REQUEST_STATE_ONLY_LEG_PACING_MS3, outerSignal);
81431
81568
  let ctxNext = {
81432
81569
  ...ctx,
81433
81570
  mcpReq: {
@@ -84784,8 +84921,8 @@ var NodeStreamableHTTPServerTransport2 = class {
84784
84921
 
84785
84922
  // src/computers.ts
84786
84923
  import { randomBytes as randomBytes5 } from "node:crypto";
84787
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
84788
- import { isAbsolute, join as join9, resolve as resolve4 } from "node:path";
84924
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
84925
+ import { isAbsolute, join as join10, resolve as resolve4 } from "node:path";
84789
84926
  var READ = ["about", "read", "list", "search"];
84790
84927
  var FILES = [...READ, "edit"];
84791
84928
  var TOOLS2 = {
@@ -84793,29 +84930,37 @@ var TOOLS2 = {
84793
84930
  files: FILES,
84794
84931
  terminal: [...FILES, "exec_command", "write_stdin", "list_sessions"]
84795
84932
  };
84796
- var MAX_LOG = 256 * 1024;
84933
+ var MAX_LOG2 = 256 * 1024;
84797
84934
  var MAX_TIMER_S = 2e6;
84798
84935
  var TICKET_MS = 6e4;
84799
84936
  var expiry = (seconds2) => new Date(Date.now() + seconds2 * 1e3).toISOString();
84937
+ var messageOf = (error62) => error62 instanceof Error ? error62.message : String(error62);
84938
+ var refusalOf = (error62) => error62 instanceof ApiError ? error62 : new ApiError(500, messageOf(error62));
84800
84939
  var Computers = class {
84801
84940
  #data;
84802
84941
  #base;
84803
- #runtime;
84942
+ #runtimes;
84943
+ /** The isolation of a computer made without saying which. */
84944
+ isolation;
84804
84945
  #stored = /* @__PURE__ */ new Map();
84805
84946
  #live = /* @__PURE__ */ new Map();
84806
84947
  #tickets = /* @__PURE__ */ new Map();
84807
84948
  #file;
84808
- constructor(data, base, runtime) {
84949
+ constructor(data, base, runtimes, isolation) {
84809
84950
  this.#data = data;
84810
84951
  this.#base = base;
84811
- this.#runtime = runtime;
84812
- this.#file = join9(data, "computers.json");
84952
+ this.#runtimes = runtimes;
84953
+ this.isolation = isolation;
84954
+ this.#file = join10(data, "computers.json");
84813
84955
  if (existsSync2(this.#file))
84814
- for (const entry of JSON.parse(readFileSync4(this.#file, "utf8")))
84815
- this.#stored.set(entry.id, entry);
84956
+ for (const entry of JSON.parse(readFileSync5(this.#file, "utf8")))
84957
+ this.#stored.set(entry.id, { ...entry, isolation: entry.isolation ?? isolation });
84816
84958
  }
84817
- get isolation() {
84818
- return this.#runtime.isolation;
84959
+ /** The isolations a computer can be made in now. */
84960
+ async available() {
84961
+ const all = Object.keys(this.#runtimes);
84962
+ const can = await Promise.all(all.map((one) => this.#runtimes[one].available()));
84963
+ return all.filter((_, index) => can[index]);
84819
84964
  }
84820
84965
  /** Bring back what was running when the service last stopped. */
84821
84966
  async resume() {
@@ -84823,12 +84968,17 @@ var Computers = class {
84823
84968
  if (stored.state !== "running") continue;
84824
84969
  if (stored.endAt && Date.parse(stored.endAt) <= Date.now()) {
84825
84970
  stored.state = "stopped";
84826
- await this.#runtime.stopLeftover(stored.id);
84971
+ await this.#runtimes[stored.isolation].stopLeftover(stored.id).catch(
84972
+ (error62) => this.#log(stored.id, `What it left running wasn't stopped: ${messageOf(error62)}`)
84973
+ );
84827
84974
  continue;
84828
84975
  }
84829
- await this.#start(stored).catch(
84830
- (error62) => this.#log(stored.id, `It didn't start again: ${String(error62)}`)
84831
- );
84976
+ await this.#start(stored).catch((error62) => {
84977
+ const live = this.#live.get(stored.id);
84978
+ if (live) live.failed = true;
84979
+ this.#arm(stored);
84980
+ this.#log(stored.id, `It didn't start again: ${messageOf(error62)}`);
84981
+ });
84832
84982
  }
84833
84983
  this.#save();
84834
84984
  }
@@ -84841,7 +84991,7 @@ var Computers = class {
84841
84991
  if (input2.workspace !== void 0) {
84842
84992
  let folder = false;
84843
84993
  try {
84844
- folder = isAbsolute(input2.workspace) && statSync3(input2.workspace).isDirectory();
84994
+ folder = isAbsolute(input2.workspace) && statSync4(input2.workspace).isDirectory();
84845
84995
  } catch {
84846
84996
  }
84847
84997
  if (!folder) throw new ApiError(400, `There's no folder at ${input2.workspace}.`);
@@ -84854,6 +85004,7 @@ var Computers = class {
84854
85004
  id,
84855
85005
  name: name ?? id,
84856
85006
  access,
85007
+ isolation: input2.isolation ?? this.isolation,
84857
85008
  browser,
84858
85009
  ...workspace ? { workspace } : {},
84859
85010
  metadata: metadata ?? {},
@@ -84864,7 +85015,7 @@ var Computers = class {
84864
85015
  ...timeout === void 0 ? {} : { endAt: expiry(timeout) }
84865
85016
  };
84866
85017
  if (!workspace && access !== "none") {
84867
- stored.workspace = join9(this.#folder(id), "workspace");
85018
+ stored.workspace = join10(this.#folder(id), "workspace");
84868
85019
  mkdirSync2(stored.workspace, { recursive: true, mode: 448 });
84869
85020
  }
84870
85021
  this.#stored.set(id, stored);
@@ -84873,9 +85024,10 @@ var Computers = class {
84873
85024
  } catch (error62) {
84874
85025
  await this.#halt(id);
84875
85026
  this.#stored.delete(id);
84876
- await this.#runtime.remove(id);
85027
+ await this.#runtimes[stored.isolation].remove(id).catch(() => {
85028
+ });
84877
85029
  rmSync2(this.#folder(id), { recursive: true, force: true });
84878
- throw new ApiError(500, error62 instanceof Error ? error62.message : String(error62));
85030
+ throw refusalOf(error62);
84879
85031
  }
84880
85032
  this.#save();
84881
85033
  return { ...this.#describe(stored), accessToken: token };
@@ -84930,16 +85082,22 @@ var Computers = class {
84930
85082
  await this.#halt(id);
84931
85083
  stored.state = "stopped";
84932
85084
  this.#save();
84933
- throw new ApiError(500, error62 instanceof Error ? error62.message : String(error62));
85085
+ throw refusalOf(error62);
84934
85086
  }
84935
85087
  this.#save();
84936
85088
  return this.#describe(stored);
84937
85089
  }
84938
85090
  /** Stop everything and remove what the service made. A folder someone named stays. */
84939
85091
  async kill(id) {
84940
- this.#found(id);
85092
+ const stored = this.#found(id);
84941
85093
  await this.#halt(id);
84942
- await this.#runtime.remove(id);
85094
+ try {
85095
+ await this.#runtimes[stored.isolation].remove(id);
85096
+ } catch (error62) {
85097
+ stored.state = "stopped";
85098
+ this.#save();
85099
+ throw refusalOf(error62);
85100
+ }
84943
85101
  this.#stored.delete(id);
84944
85102
  this.#save();
84945
85103
  rmSync2(this.#folder(id), { recursive: true, force: true });
@@ -84948,7 +85106,7 @@ var Computers = class {
84948
85106
  this.#found(id);
84949
85107
  let text3 = "";
84950
85108
  try {
84951
- text3 = readFileSync4(this.#logFile(id), "utf8");
85109
+ text3 = readFileSync5(this.#logFile(id), "utf8");
84952
85110
  } catch {
84953
85111
  }
84954
85112
  return text3.split("\n").filter(Boolean).map((entry) => ({
@@ -84965,17 +85123,17 @@ var Computers = class {
84965
85123
  throw new ApiError(404, "This computer has no files or terminal.");
84966
85124
  if (endpoint === "browser" && !stored.browser)
84967
85125
  throw new ApiError(404, "This computer has no browser.");
84968
- const running = this.#live.get(id)?.running;
84969
- if (!running) throw new ApiError(409, "It has stopped.");
85126
+ const running2 = this.#live.get(id)?.running;
85127
+ if (!running2) throw new ApiError(409, "It has stopped.");
84970
85128
  this.#log(id, `${request.method} mcp/${endpoint}`);
84971
- return running.handle(endpoint, request, response);
85129
+ return running2.handle(endpoint, request, response);
84972
85130
  }
84973
85131
  /** A one-time ticket to open one of its surfaces live. */
84974
85132
  liveSession(id, surface, mode) {
84975
85133
  this.#found(id);
84976
- const running = this.#live.get(id)?.running;
84977
- if (!running) throw new ApiError(409, "It has stopped.");
84978
- const info = running.surfaces.find((one) => one.surface === surface);
85134
+ const running2 = this.#live.get(id)?.running;
85135
+ if (!running2) throw new ApiError(409, "It has stopped.");
85136
+ const info = running2.surfaces.find((one) => one.surface === surface);
84979
85137
  if (!info) throw new ApiError(409, `This computer has no ${surface} to open.`);
84980
85138
  if (info.unavailable) throw new ApiError(409, info.unavailable);
84981
85139
  if (mode === "operate" && !info.operate)
@@ -85000,10 +85158,10 @@ var Computers = class {
85000
85158
  this.#tickets.delete(ticket);
85001
85159
  if (!found || found.expiresAt <= Date.now())
85002
85160
  throw new ApiError(404, "There's no such live session: it was used, or it expired.");
85003
- const running = this.#live.get(found.computerID)?.running;
85004
- if (!running) throw new ApiError(409, "It has stopped.");
85161
+ const running2 = this.#live.get(found.computerID)?.running;
85162
+ if (!running2) throw new ApiError(409, "It has stopped.");
85005
85163
  this.#log(found.computerID, `live ${found.surface} ${found.mode}`);
85006
- await running.openLive(found.surface, found.mode, request, socket, head);
85164
+ await running2.openLive(found.surface, found.mode, request, socket, head);
85007
85165
  }
85008
85166
  /** Stop every computer, leaving their records to resume at the next start. */
85009
85167
  async close() {
@@ -85015,17 +85173,17 @@ var Computers = class {
85015
85173
  return stored;
85016
85174
  }
85017
85175
  #folder(id) {
85018
- return privateFolder(join9(this.#data, "computers", id));
85176
+ return privateFolder(join10(this.#data, "computers", id));
85019
85177
  }
85020
85178
  #logFile(id) {
85021
- return join9(this.#folder(id), "log");
85179
+ return join10(this.#folder(id), "log");
85022
85180
  }
85023
85181
  #log(id, line) {
85024
85182
  if (!this.#stored.has(id)) return;
85025
85183
  try {
85026
85184
  const file2 = this.#logFile(id);
85027
- if (existsSync2(file2) && statSync3(file2).size > MAX_LOG)
85028
- writePrivate(file2, readFileSync4(file2, "utf8").slice(-MAX_LOG / 2));
85185
+ if (existsSync2(file2) && statSync4(file2).size > MAX_LOG2)
85186
+ writePrivate(file2, readFileSync5(file2, "utf8").slice(-MAX_LOG2 / 2));
85029
85187
  appendFileSync(file2, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
85030
85188
  `, { mode: 384 });
85031
85189
  } catch {
@@ -85036,12 +85194,13 @@ var Computers = class {
85036
85194
  }
85037
85195
  #describe(stored) {
85038
85196
  const path = `${this.#base()}/v1/computers/${stored.id}/mcp`;
85039
- const down = this.#live.get(stored.id)?.failed === true;
85197
+ const down2 = this.#live.get(stored.id)?.failed === true;
85040
85198
  return {
85041
85199
  computerID: stored.id,
85042
85200
  name: stored.name,
85043
- state: stored.state === "running" && down ? "failed" : stored.state,
85201
+ state: stored.state === "running" && down2 ? "failed" : stored.state,
85044
85202
  access: stored.access,
85203
+ isolation: stored.isolation,
85045
85204
  browser: stored.browser,
85046
85205
  workspace: stored.workspace ?? null,
85047
85206
  startedAt: stored.startedAt,
@@ -85057,7 +85216,9 @@ var Computers = class {
85057
85216
  const live = {};
85058
85217
  this.#live.set(stored.id, live);
85059
85218
  stored.startedAt = (/* @__PURE__ */ new Date()).toISOString();
85060
- live.running = await this.#runtime.start(
85219
+ const runtime = this.#runtimes[stored.isolation];
85220
+ await runtime.prepare();
85221
+ live.running = await runtime.start(
85061
85222
  {
85062
85223
  id: stored.id,
85063
85224
  access: stored.access,
@@ -85110,7 +85271,7 @@ var OPENAPI = {
85110
85271
  "openapi": "3.1.0",
85111
85272
  "info": {
85112
85273
  "title": "AgentComputer",
85113
- "version": "0.0.14",
85274
+ "version": "0.0.15",
85114
85275
  "description": "Computers for agents. This file is the source of truth: the HTTP API, the MCP control tools and the SDK are all generated from it. An operation with `x-mcp-tool` is also an MCP tool of that name.",
85115
85276
  "license": {
85116
85277
  "name": "Apache-2.0",
@@ -85570,7 +85731,8 @@ var OPENAPI = {
85570
85731
  "running",
85571
85732
  "stopped",
85572
85733
  "failed"
85573
- ]
85734
+ ],
85735
+ "description": "failed: it was to be running and isn't, because its browser or container ended by itself, or because it couldn't be brought back when the service started. Its logs say why. Stop it and start it again."
85574
85736
  },
85575
85737
  "Timeout": {
85576
85738
  "type": "integer",
@@ -85611,6 +85773,10 @@ var OPENAPI = {
85611
85773
  "maxLength": 4096,
85612
85774
  "description": "The full path of a folder that exists, on the service's machine. With none, the service makes a folder, which goes when the computer is killed. A folder named here never does."
85613
85775
  },
85776
+ "isolation": {
85777
+ "$ref": "#/components/schemas/Isolation",
85778
+ "description": "How it is isolated, which it keeps for as long as it exists. Left out, it is the service's own, the `isolation` that `getInfo` answers."
85779
+ },
85614
85780
  "timeout": {
85615
85781
  "$ref": "#/components/schemas/Timeout"
85616
85782
  },
@@ -85619,6 +85785,14 @@ var OPENAPI = {
85619
85785
  }
85620
85786
  }
85621
85787
  },
85788
+ "Isolation": {
85789
+ "type": "string",
85790
+ "enum": [
85791
+ "none",
85792
+ "container"
85793
+ ],
85794
+ "description": "none: processes of whoever started the service. container: a container of its own, which needs Docker on the service's machine."
85795
+ },
85622
85796
  "Endpoints": {
85623
85797
  "type": "object",
85624
85798
  "required": [
@@ -85650,6 +85824,7 @@ var OPENAPI = {
85650
85824
  "name",
85651
85825
  "state",
85652
85826
  "access",
85827
+ "isolation",
85653
85828
  "browser",
85654
85829
  "workspace",
85655
85830
  "startedAt",
@@ -85670,6 +85845,10 @@ var OPENAPI = {
85670
85845
  "access": {
85671
85846
  "$ref": "#/components/schemas/Access"
85672
85847
  },
85848
+ "isolation": {
85849
+ "$ref": "#/components/schemas/Isolation",
85850
+ "description": "The isolation it was made in. It is never run in another."
85851
+ },
85673
85852
  "browser": {
85674
85853
  "type": "boolean"
85675
85854
  },
@@ -86828,19 +87007,23 @@ var OPENAPI = {
86828
87007
  "type": "object",
86829
87008
  "required": [
86830
87009
  "version",
86831
- "isolation"
87010
+ "isolation",
87011
+ "isolationsAvailable"
86832
87012
  ],
86833
87013
  "properties": {
86834
87014
  "version": {
86835
87015
  "type": "string"
86836
87016
  },
86837
87017
  "isolation": {
86838
- "type": "string",
86839
- "enum": [
86840
- "none",
86841
- "container"
86842
- ],
86843
- "description": "How this service isolates its computers, chosen when it is started."
87018
+ "$ref": "#/components/schemas/Isolation",
87019
+ "description": "The isolation of a computer made without saying which, chosen when the service is started."
87020
+ },
87021
+ "isolationsAvailable": {
87022
+ "type": "array",
87023
+ "items": {
87024
+ "$ref": "#/components/schemas/Isolation"
87025
+ },
87026
+ "description": "The isolations a computer can be made in now, `none` first. `container` is among them while Docker answers on the service's machine. It is what to offer, not a promise: Docker is asked for a second here and for ten when a container is made, so one that is slow can be missing here and still be made. If `isolation` itself is missing, a computer made without saying which is refused with 409."
86844
87027
  }
86845
87028
  }
86846
87029
  }
@@ -86868,7 +87051,8 @@ var OPERATIONS = [
86868
87051
  "type": "object",
86869
87052
  "required": [
86870
87053
  "version",
86871
- "isolation"
87054
+ "isolation",
87055
+ "isolationsAvailable"
86872
87056
  ],
86873
87057
  "properties": {
86874
87058
  "version": {
@@ -86880,7 +87064,19 @@ var OPERATIONS = [
86880
87064
  "none",
86881
87065
  "container"
86882
87066
  ],
86883
- "description": "How this service isolates its computers, chosen when it is started."
87067
+ "description": "The isolation of a computer made without saying which, chosen when the service is started."
87068
+ },
87069
+ "isolationsAvailable": {
87070
+ "type": "array",
87071
+ "items": {
87072
+ "type": "string",
87073
+ "enum": [
87074
+ "none",
87075
+ "container"
87076
+ ],
87077
+ "description": "none: processes of whoever started the service. container: a container of its own, which needs Docker on the service's machine."
87078
+ },
87079
+ "description": "The isolations a computer can be made in now, `none` first. `container` is among them while Docker answers on the service's machine. It is what to offer, not a promise: Docker is asked for a second here and for ten when a container is made, so one that is slow can be missing here and still be made. If `isolation` itself is missing, a computer made without saying which is refused with 409."
86884
87080
  }
86885
87081
  }
86886
87082
  },
@@ -86908,7 +87104,8 @@ var OPERATIONS = [
86908
87104
  "running",
86909
87105
  "stopped",
86910
87106
  "failed"
86911
- ]
87107
+ ],
87108
+ "description": "failed: it was to be running and isn't, because its browser or container ended by itself, or because it couldn't be brought back when the service started. Its logs say why. Stop it and start it again."
86912
87109
  }
86913
87110
  },
86914
87111
  "required": []
@@ -86922,6 +87119,7 @@ var OPERATIONS = [
86922
87119
  "name",
86923
87120
  "state",
86924
87121
  "access",
87122
+ "isolation",
86925
87123
  "browser",
86926
87124
  "workspace",
86927
87125
  "startedAt",
@@ -86942,7 +87140,8 @@ var OPERATIONS = [
86942
87140
  "running",
86943
87141
  "stopped",
86944
87142
  "failed"
86945
- ]
87143
+ ],
87144
+ "description": "failed: it was to be running and isn't, because its browser or container ended by itself, or because it couldn't be brought back when the service started. Its logs say why. Stop it and start it again."
86946
87145
  },
86947
87146
  "access": {
86948
87147
  "type": "string",
@@ -86954,6 +87153,14 @@ var OPERATIONS = [
86954
87153
  ],
86955
87154
  "description": "How far it reaches into files and commands, each level containing the one before: `read` has about, read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
86956
87155
  },
87156
+ "isolation": {
87157
+ "type": "string",
87158
+ "enum": [
87159
+ "none",
87160
+ "container"
87161
+ ],
87162
+ "description": "The isolation it was made in. It is never run in another."
87163
+ },
86957
87164
  "browser": {
86958
87165
  "type": "boolean"
86959
87166
  },
@@ -87053,6 +87260,14 @@ var OPERATIONS = [
87053
87260
  "maxLength": 4096,
87054
87261
  "description": "The full path of a folder that exists, on the service's machine. With none, the service makes a folder, which goes when the computer is killed. A folder named here never does."
87055
87262
  },
87263
+ "isolation": {
87264
+ "type": "string",
87265
+ "enum": [
87266
+ "none",
87267
+ "container"
87268
+ ],
87269
+ "description": "How it is isolated, which it keeps for as long as it exists. Left out, it is the service's own, the `isolation` that `getInfo` answers."
87270
+ },
87056
87271
  "timeout": {
87057
87272
  "type": "integer",
87058
87273
  "minimum": 1,
@@ -87081,6 +87296,7 @@ var OPERATIONS = [
87081
87296
  "name",
87082
87297
  "state",
87083
87298
  "access",
87299
+ "isolation",
87084
87300
  "browser",
87085
87301
  "workspace",
87086
87302
  "startedAt",
@@ -87101,7 +87317,8 @@ var OPERATIONS = [
87101
87317
  "running",
87102
87318
  "stopped",
87103
87319
  "failed"
87104
- ]
87320
+ ],
87321
+ "description": "failed: it was to be running and isn't, because its browser or container ended by itself, or because it couldn't be brought back when the service started. Its logs say why. Stop it and start it again."
87105
87322
  },
87106
87323
  "access": {
87107
87324
  "type": "string",
@@ -87113,6 +87330,14 @@ var OPERATIONS = [
87113
87330
  ],
87114
87331
  "description": "How far it reaches into files and commands, each level containing the one before: `read` has about, read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
87115
87332
  },
87333
+ "isolation": {
87334
+ "type": "string",
87335
+ "enum": [
87336
+ "none",
87337
+ "container"
87338
+ ],
87339
+ "description": "The isolation it was made in. It is never run in another."
87340
+ },
87116
87341
  "browser": {
87117
87342
  "type": "boolean"
87118
87343
  },
@@ -87221,6 +87446,7 @@ var OPERATIONS = [
87221
87446
  "name",
87222
87447
  "state",
87223
87448
  "access",
87449
+ "isolation",
87224
87450
  "browser",
87225
87451
  "workspace",
87226
87452
  "startedAt",
@@ -87241,7 +87467,8 @@ var OPERATIONS = [
87241
87467
  "running",
87242
87468
  "stopped",
87243
87469
  "failed"
87244
- ]
87470
+ ],
87471
+ "description": "failed: it was to be running and isn't, because its browser or container ended by itself, or because it couldn't be brought back when the service started. Its logs say why. Stop it and start it again."
87245
87472
  },
87246
87473
  "access": {
87247
87474
  "type": "string",
@@ -87253,6 +87480,14 @@ var OPERATIONS = [
87253
87480
  ],
87254
87481
  "description": "How far it reaches into files and commands, each level containing the one before: `read` has about, read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
87255
87482
  },
87483
+ "isolation": {
87484
+ "type": "string",
87485
+ "enum": [
87486
+ "none",
87487
+ "container"
87488
+ ],
87489
+ "description": "The isolation it was made in. It is never run in another."
87490
+ },
87256
87491
  "browser": {
87257
87492
  "type": "boolean"
87258
87493
  },
@@ -87452,6 +87687,7 @@ var OPERATIONS = [
87452
87687
  "name",
87453
87688
  "state",
87454
87689
  "access",
87690
+ "isolation",
87455
87691
  "browser",
87456
87692
  "workspace",
87457
87693
  "startedAt",
@@ -87472,7 +87708,8 @@ var OPERATIONS = [
87472
87708
  "running",
87473
87709
  "stopped",
87474
87710
  "failed"
87475
- ]
87711
+ ],
87712
+ "description": "failed: it was to be running and isn't, because its browser or container ended by itself, or because it couldn't be brought back when the service started. Its logs say why. Stop it and start it again."
87476
87713
  },
87477
87714
  "access": {
87478
87715
  "type": "string",
@@ -87484,6 +87721,14 @@ var OPERATIONS = [
87484
87721
  ],
87485
87722
  "description": "How far it reaches into files and commands, each level containing the one before: `read` has about, read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
87486
87723
  },
87724
+ "isolation": {
87725
+ "type": "string",
87726
+ "enum": [
87727
+ "none",
87728
+ "container"
87729
+ ],
87730
+ "description": "The isolation it was made in. It is never run in another."
87731
+ },
87487
87732
  "browser": {
87488
87733
  "type": "boolean"
87489
87734
  },
@@ -87576,6 +87821,7 @@ var OPERATIONS = [
87576
87821
  "name",
87577
87822
  "state",
87578
87823
  "access",
87824
+ "isolation",
87579
87825
  "browser",
87580
87826
  "workspace",
87581
87827
  "startedAt",
@@ -87596,7 +87842,8 @@ var OPERATIONS = [
87596
87842
  "running",
87597
87843
  "stopped",
87598
87844
  "failed"
87599
- ]
87845
+ ],
87846
+ "description": "failed: it was to be running and isn't, because its browser or container ended by itself, or because it couldn't be brought back when the service started. Its logs say why. Stop it and start it again."
87600
87847
  },
87601
87848
  "access": {
87602
87849
  "type": "string",
@@ -87608,6 +87855,14 @@ var OPERATIONS = [
87608
87855
  ],
87609
87856
  "description": "How far it reaches into files and commands, each level containing the one before: `read` has about, read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
87610
87857
  },
87858
+ "isolation": {
87859
+ "type": "string",
87860
+ "enum": [
87861
+ "none",
87862
+ "container"
87863
+ ],
87864
+ "description": "The isolation it was made in. It is never run in another."
87865
+ },
87611
87866
  "browser": {
87612
87867
  "type": "boolean"
87613
87868
  },
@@ -87705,6 +87960,7 @@ var OPERATIONS = [
87705
87960
  "name",
87706
87961
  "state",
87707
87962
  "access",
87963
+ "isolation",
87708
87964
  "browser",
87709
87965
  "workspace",
87710
87966
  "startedAt",
@@ -87725,7 +87981,8 @@ var OPERATIONS = [
87725
87981
  "running",
87726
87982
  "stopped",
87727
87983
  "failed"
87728
- ]
87984
+ ],
87985
+ "description": "failed: it was to be running and isn't, because its browser or container ended by itself, or because it couldn't be brought back when the service started. Its logs say why. Stop it and start it again."
87729
87986
  },
87730
87987
  "access": {
87731
87988
  "type": "string",
@@ -87737,6 +87994,14 @@ var OPERATIONS = [
87737
87994
  ],
87738
87995
  "description": "How far it reaches into files and commands, each level containing the one before: `read` has about, read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
87739
87996
  },
87997
+ "isolation": {
87998
+ "type": "string",
87999
+ "enum": [
88000
+ "none",
88001
+ "container"
88002
+ ],
88003
+ "description": "The isolation it was made in. It is never run in another."
88004
+ },
87740
88005
  "browser": {
87741
88006
  "type": "boolean"
87742
88007
  },
@@ -88024,9 +88289,12 @@ async function serve(options) {
88024
88289
  checkExposure(host, options);
88025
88290
  const data = privateFolder(options.data);
88026
88291
  const control = digestOf(controlToken(data));
88292
+ const live = await running(data);
88293
+ if (live)
88294
+ throw new Error(`A service is already running with its data in ${data}, at ${live.url}.`);
88027
88295
  let base = "";
88028
- await options.runtime.prepare();
88029
- const computers = new Computers(data, () => base, options.runtime);
88296
+ await options.runtimes[options.isolation].prepare();
88297
+ const computers = new Computers(data, () => base, options.runtimes, options.isolation);
88030
88298
  const handlers = createHandlers(computers);
88031
88299
  const routes = OPERATIONS.map((operation) => ({
88032
88300
  operation,
@@ -88114,7 +88382,7 @@ async function serve(options) {
88114
88382
  });
88115
88383
  };
88116
88384
  const server = options.tls ? createTlsServer(
88117
- { cert: readFileSync5(options.tls.cert), key: readFileSync5(options.tls.key) },
88385
+ { cert: readFileSync6(options.tls.cert), key: readFileSync6(options.tls.key) },
88118
88386
  listener
88119
88387
  ) : createServer5(listener);
88120
88388
  const allowed = new Set(options.allowOrigins ?? []);
@@ -88146,11 +88414,12 @@ ${JSON.stringify({ code: status, message })}
88146
88414
  const bound = server.address();
88147
88415
  const scheme = options.tls ? "https" : "http";
88148
88416
  base = options.publicUrl?.replace(/\/+$/, "") ?? `${scheme}://${bound.family === "IPv6" ? `[${bound.address}]` : bound.address}:${bound.port}`;
88417
+ const record3 = recordOf(data);
88418
+ rmSync3(record3, { force: true });
88149
88419
  await computers.resume();
88150
- const record3 = join10(data, "server.json");
88151
88420
  writePrivate(
88152
88421
  record3,
88153
- JSON.stringify({ url: base, pid: process.pid, isolation: options.runtime.isolation })
88422
+ JSON.stringify({ url: base, pid: process.pid, isolation: options.isolation })
88154
88423
  );
88155
88424
  process.stdout.write(`AgentComputer on ${base}, data in ${data}
88156
88425
  `);
@@ -88176,9 +88445,14 @@ var USAGE = `Usage: agentcomputer <command> [--data DIR]
88176
88445
  [--browser-name NAME] [--browser-path PATH]
88177
88446
  run the service; 127.0.0.1:7878 by default, and
88178
88447
  anywhere else only with TLS
88179
- info the service's version and isolation
88448
+ up [the options of serve] run the service in the background and return once it
88449
+ answers, printing where; one running is left as it is
88450
+ down stop the service, and its computers with it
88451
+ status where the service is running, if it is
88452
+ info the service's version, the isolation it gives a computer
88453
+ made without saying, and the ones it can make now
88180
88454
  create --access none|read|files|terminal [--browser] [--workspace DIR] [--name NAME]
88181
- [--timeout SECONDS] [--metadata KEY=VALUE]...
88455
+ [--isolation none|container] [--timeout SECONDS] [--metadata KEY=VALUE]...
88182
88456
  make a computer; prints its endpoints and token, once
88183
88457
  list [--state running|stopped|failed]
88184
88458
  the computers
@@ -88191,6 +88465,8 @@ var USAGE = `Usage: agentcomputer <command> [--data DIR]
88191
88465
  a one-time ticket to open one live, as a WebSocket
88192
88466
  kill <id> stop it and remove what the service made
88193
88467
 
88468
+ --version prints this program's version, with no service.
88469
+
88194
88470
  The data folder is --data, else $AGENTCOMPUTER_DATA, else $XDG_DATA_HOME/agentcomputer, else
88195
88471
  ~/.local/share/agentcomputer. Commands find the running service there, with its control token.
88196
88472
  `;
@@ -88203,20 +88479,18 @@ function dataFolder(given) {
88203
88479
  given ?? process.env.AGENTCOMPUTER_DATA ?? (xdg && isAbsolute2(xdg) ? join11(xdg, "agentcomputer") : join11(homedir2(), ".local", "share", "agentcomputer"))
88204
88480
  );
88205
88481
  }
88482
+ var none = (data) => `No service is running with its data in ${data}. Start one: agentcomputer up`;
88206
88483
  function connectionTo(data) {
88207
- try {
88208
- const { url: url3 } = JSON.parse(readFileSync6(join11(data, "server.json"), "utf8"));
88209
- return { apiUrl: url3, apiKey: readFileSync6(join11(data, "control-token"), "utf8").trim() };
88210
- } catch {
88211
- throw new Error(
88212
- `No service is running with its data in ${data}. Start one: agentcomputer serve`
88213
- );
88214
- }
88484
+ const said = recorded(data);
88485
+ if (!said) throw new Error(none(data));
88486
+ return { apiUrl: said.url, apiKey: said.token };
88215
88487
  }
88216
88488
  async function main() {
88217
- const { values, positionals } = parseArgs({
88489
+ const { values, positionals, tokens } = parseArgs({
88218
88490
  allowPositionals: true,
88491
+ tokens: true,
88219
88492
  options: {
88493
+ version: { type: "boolean" },
88220
88494
  data: { type: "string" },
88221
88495
  listen: { type: "string" },
88222
88496
  isolation: { type: "string" },
@@ -88247,19 +88521,38 @@ async function main() {
88247
88521
  }
88248
88522
  });
88249
88523
  const [command, id, extra] = positionals;
88524
+ if (values.version) return void process.stdout.write(`${VERSION2}
88525
+ `);
88250
88526
  if (values.help || !command) return void process.stdout.write(USAGE);
88251
88527
  const data = dataFolder(values.data);
88252
88528
  const out = (value) => process.stdout.write(`${JSON.stringify(value, null, 2)}
88253
88529
  `);
88530
+ if (command === "status") {
88531
+ const found = await running(data);
88532
+ if (!found) throw new Error(none(data));
88533
+ return out(found);
88534
+ }
88535
+ if (command === "up") {
88536
+ const given = process.argv.slice(2);
88537
+ given.splice(tokens.find((token) => token.kind === "positional")?.index, 1);
88538
+ return out(await up(data, process.argv[1], given));
88539
+ }
88540
+ if (command === "down") return down(data);
88541
+ if ((command === "start" || command === "stop") && !id) {
88542
+ const service = command === "start" ? "up" : "down";
88543
+ throw new UsageError(
88544
+ `${command} takes a computer's id. For the service itself: agentcomputer ${service}`
88545
+ );
88546
+ }
88254
88547
  const idOf = () => {
88255
88548
  if (!id) throw new UsageError(`${command} takes a computer's id.`);
88256
88549
  return id;
88257
88550
  };
88258
88551
  const milliseconds = (seconds2) => Number(seconds2) * 1e3;
88552
+ const { isolation } = values;
88553
+ if (isolation !== void 0 && isolation !== "none" && isolation !== "container")
88554
+ throw new UsageError("--isolation is none or container.");
88259
88555
  if (command === "serve") {
88260
- const isolation = values.isolation ?? "none";
88261
- if (isolation !== "none" && isolation !== "container")
88262
- throw new UsageError("--isolation is none or container.");
88263
88556
  const { "tls-cert": cert, "tls-key": key } = values;
88264
88557
  if (Boolean(cert) !== Boolean(key))
88265
88558
  throw new UsageError("--tls-cert and --tls-key go together.");
@@ -88270,10 +88563,11 @@ async function main() {
88270
88563
  tlsProxy: values["tls-proxy"],
88271
88564
  publicUrl: values["public-url"],
88272
88565
  allowOrigins: values["allow-origin"],
88273
- runtime: isolation === "container" ? new ContainerRuntime(values.image ?? `${IMAGE}:${VERSION2}`) : new ProcessRuntime({
88274
- browser: values["browser-name"],
88275
- path: values["browser-path"]
88276
- })
88566
+ isolation: isolation ?? "none",
88567
+ runtimes: {
88568
+ none: new ProcessRuntime({ browser: values["browser-name"], path: values["browser-path"] }),
88569
+ container: new ContainerRuntime(values.image ?? `${IMAGE}:${VERSION2}`)
88570
+ }
88277
88571
  });
88278
88572
  }
88279
88573
  if (command === "browser-host")
@@ -88303,6 +88597,7 @@ async function main() {
88303
88597
  ...connection,
88304
88598
  access: values.access,
88305
88599
  ...values.browser ? { browser: true } : {},
88600
+ ...isolation ? { isolation } : {},
88306
88601
  ...values.workspace ? { workspace: resolve5(values.workspace) } : {},
88307
88602
  ...values.name ? { name: values.name } : {},
88308
88603
  ...values.timeout ? { timeoutMs: milliseconds(values.timeout) } : {},