@agentorg/computer 0.0.15 → 0.0.16

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.
package/README.md CHANGED
@@ -66,7 +66,7 @@ The file tools stay inside the workspace. That's a check, not a boundary. Below
66
66
 
67
67
  Before 0.0.14 the browser's endpoint passed on a body it couldn't read as one request, unchecked. So on a computer made with `--access none`, `read` or `files` and `--browser`, a caller holding that computer's token could run code in its browser by sending the call in a batch or behind a byte order mark. It needed the computer's own token and a client that sends such a body, which MCP clients don't. 0.0.14 fixes it; a computer in a container made by an earlier version keeps the fault until it is killed and made again.
68
68
 
69
- `--browser` drives the Google Chrome installed on the machine, in a profile of the computer's own. `serve --browser-name` and `--browser-path` choose another. The browser has no debug port: a small host process starts it and shares it on a Unix socket in a folder only its user can enter, which the agent's tools and the live view both use. It lives as long as its computer does: a client that ends its session or goes away leaves the browser and its pages as they are, and the agent's `browser_close` is refused. Only an agent with `--access terminal` can stop it, with `browser_run_code_unsafe` or from its terminal.
69
+ `--browser` drives the Google Chrome installed on the machine, in a profile of the computer's own. `serve --browser-name` and `--browser-path` choose another. The browser has no debug port: a small host process starts it and shares it on a Unix socket in a folder only its user can enter, which the agent's tools and the live view both use. Playwright MCP, which serves the tools, listens on a Unix socket in a second such folder, and no port either. Only the service's user can reach them; callers go through the computer's `browser` endpoint and its token. It lives as long as its computer does: a client that ends its session or goes away leaves the browser and its pages as they are, and the agent's `browser_close` is refused. Only an agent with `--access terminal` can stop it, with `browser_run_code_unsafe` or from its terminal.
70
70
 
71
71
  ## Commands
72
72
 
@@ -214,7 +214,6 @@ A shell of your own needs [node-pty](https://github.com/microsoft/node-pty), an
214
214
 
215
215
  ## Known limits
216
216
 
217
- - With no isolation, each browser's Playwright MCP listens on a loopback port with no token of its own, so another user on the same machine could reach it directly. Don't run that on a machine you share.
218
217
  - A container has no CPU or memory limit, and its network is Docker's default, so it reaches the internet and anything else Docker's network does.
219
218
  - In a container the browser runs without its own sandbox, which needs privileges a computer doesn't have. The container is the boundary.
220
219
  - A browser that stops, or a container that ends by itself, shows the computer as `failed`, and what can no longer be opened live says so in `surfaces`. So does a computer that couldn't be brought back when the service started, for one in a container because Docker didn't answer; its `logs` say why. Stop the computer and start it again.
@@ -3804,44 +3804,73 @@ async function hostBrowser(options) {
3804
3804
  }
3805
3805
 
3806
3806
  // src/browser-mcp.ts
3807
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
3807
3808
  import { createRequire as createRequire2 } from "node:module";
3809
+ import { join as join2 } from "node:path";
3808
3810
  var require2 = createRequire2(import.meta.url);
3809
3811
  var mcpParts = () => require2("playwright-core/lib/coreBundle").tools;
3812
+ function listenOnSocket(socket) {
3813
+ const http = require2("node:http");
3814
+ const make = http.createServer;
3815
+ http.createServer = ((...args) => {
3816
+ http.createServer = make;
3817
+ const server = make(...args);
3818
+ const listen = server.listen.bind(server);
3819
+ server.listen = (() => listen(socket));
3820
+ server.address = () => ({ address: "127.0.0.1", family: "IPv4", port: 0 });
3821
+ return server;
3822
+ });
3823
+ }
3810
3824
  async function serveBrowserMcp(options) {
3811
3825
  const parts = mcpParts();
3812
3826
  parts.setupExitWatchdog();
3827
+ const folder = mkdtempSync2(join2("/tmp", "agentcomputer-"));
3828
+ process.once("exit", () => rmSync2(folder, { recursive: true, force: true }));
3829
+ const socket = join2(folder, "mcp.sock");
3813
3830
  const config3 = await parts.resolveCLIConfigForMCP({
3814
3831
  endpoint: options.endpoint,
3815
3832
  outputDir: options.outputDir,
3816
3833
  host: "127.0.0.1",
3817
3834
  port: 0,
3835
+ // What the proxy sends, which is all that can reach the socket.
3836
+ allowedHosts: ["localhost"],
3818
3837
  // The tool that closes the browser refuses, as it does for any browser its clients share.
3819
3838
  sharedBrowserContext: true
3820
3839
  });
3821
3840
  const tools = parts.filteredTools(config3);
3822
- await parts.start(
3823
- {
3824
- name: "Playwright",
3825
- nameInConfig: "playwright",
3826
- // playwright-core's, as Playwright MCP's program gives it.
3827
- version: require2("playwright-core/package.json").version,
3828
- toolSchemas: tools.map((tool) => tool.schema),
3829
- ignoreRoots: config3.ignoreRoots,
3830
- /** One for each client's session: a connection of its own to the one browser. */
3831
- create: async () => {
3832
- const browser = await playwright().chromium.connect(options.endpoint);
3833
- const context = browser.contexts()[0];
3834
- if (!context) {
3835
- await browser.close();
3836
- throw new Error("The browser has no context to work in.");
3837
- }
3838
- return new parts.BrowserBackend(config3, context, tools, {
3839
- dispose: () => browser.close()
3840
- });
3841
- }
3842
- },
3843
- config3.server
3844
- );
3841
+ listenOnSocket(socket);
3842
+ const say = console.error;
3843
+ console.error = () => {
3844
+ };
3845
+ try {
3846
+ await parts.start(
3847
+ {
3848
+ name: "Playwright",
3849
+ nameInConfig: "playwright",
3850
+ // playwright-core's, as Playwright MCP's program gives it.
3851
+ version: require2("playwright-core/package.json").version,
3852
+ toolSchemas: tools.map((tool) => tool.schema),
3853
+ ignoreRoots: config3.ignoreRoots,
3854
+ /** One for each client's session: a connection of its own to the one browser. */
3855
+ create: async () => {
3856
+ const browser = await playwright().chromium.connect(options.endpoint);
3857
+ const context = browser.contexts()[0];
3858
+ if (!context) {
3859
+ await browser.close();
3860
+ throw new Error("The browser has no context to work in.");
3861
+ }
3862
+ return new parts.BrowserBackend(config3, context, tools, {
3863
+ dispose: () => browser.close()
3864
+ });
3865
+ }
3866
+ },
3867
+ config3.server
3868
+ );
3869
+ } finally {
3870
+ console.error = say;
3871
+ }
3872
+ process.stdout.write(`${JSON.stringify({ socket })}
3873
+ `);
3845
3874
  }
3846
3875
 
3847
3876
  // src/container.ts
@@ -3856,7 +3885,9 @@ import { promisify as promisify2 } from "node:util";
3856
3885
 
3857
3886
  // src/browser.ts
3858
3887
  import { spawn } from "node:child_process";
3859
- import { join as join2 } from "node:path";
3888
+ import { rmSync as rmSync3 } from "node:fs";
3889
+ import { request as ask } from "node:http";
3890
+ import { dirname, join as join3 } from "node:path";
3860
3891
  import { createInterface } from "node:readline";
3861
3892
  import { pipeline, Readable } from "node:stream";
3862
3893
  import { fileURLToPath } from "node:url";
@@ -27982,6 +28013,42 @@ function sendAnswer(upstream, response) {
27982
28013
  pipeline(Readable.fromWeb(upstream.body), response, () => {
27983
28014
  });
27984
28015
  }
28016
+ function socketFetch(socket) {
28017
+ return (url3, init = {}) => new Promise((done, failed) => {
28018
+ const { body } = init;
28019
+ if (body != null && typeof body !== "string" && !(body instanceof Uint8Array))
28020
+ return failed(new TypeError("A body to the browser's server is text or bytes."));
28021
+ const { pathname, search: search2 } = new URL(url3);
28022
+ const headers = Object.fromEntries(new Headers(init.headers));
28023
+ if (body != null) headers["content-length"] = String(Buffer.byteLength(body));
28024
+ const sent = ask(
28025
+ {
28026
+ socketPath: socket,
28027
+ path: pathname + search2,
28028
+ method: init.method ?? "GET",
28029
+ headers,
28030
+ signal: init.signal ?? void 0
28031
+ },
28032
+ (answer) => {
28033
+ const answered2 = new Headers();
28034
+ for (const [name, value] of Object.entries(answer.headers))
28035
+ for (const one of [value ?? []].flat()) answered2.append(name, one);
28036
+ const status = answer.statusCode ?? 502;
28037
+ const none2 = [204, 205, 304].includes(status);
28038
+ if (none2) answer.resume();
28039
+ done(
28040
+ new Response(none2 ? null : Readable.toWeb(answer), {
28041
+ status,
28042
+ headers: answered2
28043
+ })
28044
+ );
28045
+ }
28046
+ );
28047
+ sent.on("error", failed);
28048
+ sent.end(body ?? void 0);
28049
+ });
28050
+ }
28051
+ var MCP = "http://localhost/mcp";
27985
28052
  var self = fileURLToPath(import.meta.url);
27986
28053
  function changed(text3, change) {
27987
28054
  const one = (json4) => {
@@ -28016,7 +28083,10 @@ async function end(child) {
28016
28083
  var Browser = class {
28017
28084
  #host;
28018
28085
  #mcp;
28019
- #origin;
28086
+ /** Where its server answers, by way of its Unix socket. */
28087
+ #served;
28088
+ /** The folder its server made for its socket. */
28089
+ #socketFolder;
28020
28090
  /** The bound browser's socket, for the live viewer. */
28021
28091
  endpoint;
28022
28092
  #hidden;
@@ -28038,7 +28108,7 @@ var Browser = class {
28038
28108
  self,
28039
28109
  "browser-host",
28040
28110
  "--profile",
28041
- join2(folder, "profile"),
28111
+ join3(folder, "profile"),
28042
28112
  ...config3.path ? ["--executable", config3.path] : ["--channel", config3.browser ?? "chrome"],
28043
28113
  ...config3.sandbox === false ? ["--no-sandbox"] : []
28044
28114
  ],
@@ -28061,12 +28131,12 @@ var Browser = class {
28061
28131
  });
28062
28132
  this.endpoint.catch(() => {
28063
28133
  });
28064
- this.#origin = this.endpoint.then(
28134
+ this.#served = this.endpoint.then(
28065
28135
  (endpoint) => new Promise((done, failed) => {
28066
28136
  if (this.#stopping) return failed(new Error("It stopped."));
28067
28137
  const mcp = spawn(
28068
28138
  process.execPath,
28069
- [self, "browser-mcp", "--endpoint", endpoint, "--output", join2(folder, "output")],
28139
+ [self, "browser-mcp", "--endpoint", endpoint, "--output", join3(folder, "output")],
28070
28140
  // Its own limit on which files it touches is the folder it starts in.
28071
28141
  { cwd, stdio: ["ignore", "pipe", "pipe"] }
28072
28142
  );
@@ -28077,34 +28147,39 @@ var Browser = class {
28077
28147
  (code) => failed(new Error(`The browser's server exited with ${code}.`))
28078
28148
  );
28079
28149
  mcp.once("exit", ended("The browser's server"));
28080
- for (const stream of [mcp.stdout, mcp.stderr])
28081
- createInterface({ input: stream }).on("line", (line) => {
28150
+ createInterface({ input: mcp.stdout }).on("line", (line) => {
28151
+ try {
28152
+ const { socket } = JSON.parse(line);
28153
+ log(`The browser's server is on ${socket}.`);
28154
+ this.#socketFolder = dirname(socket);
28155
+ done(socketFetch(socket));
28156
+ } catch {
28082
28157
  log(line);
28083
- const listening = /^Listening on (http:\/\/[^/\s]+)/.exec(line);
28084
- if (listening) done(listening[1]);
28085
- });
28158
+ }
28159
+ });
28160
+ createInterface({ input: mcp.stderr }).on("line", log);
28086
28161
  })
28087
28162
  );
28088
- this.#origin.catch(() => {
28163
+ this.#served.catch(() => {
28089
28164
  });
28090
28165
  }
28091
28166
  /** Wait until it answers, and read its tools. */
28092
28167
  async ready() {
28093
- const origin = await Promise.race([
28094
- this.#origin,
28168
+ const served = await Promise.race([
28169
+ this.#served,
28095
28170
  new Promise(
28096
28171
  (_, failed) => setTimeout(() => failed(new Error("The browser's server didn't start.")), START_MS)
28097
28172
  )
28098
28173
  ]);
28099
28174
  const client = new Client({ name: "agentcomputer", version: "0" });
28100
- await client.connect(new StreamableHTTPClientTransport(new URL(`${origin}/mcp`)));
28175
+ await client.connect(new StreamableHTTPClientTransport(new URL(MCP), { fetch: served }));
28101
28176
  const { tools } = await client.listTools();
28102
28177
  await client.close();
28103
28178
  this.tools = tools.map((tool) => tool.name).filter((name) => !this.#hidden.has(name));
28104
28179
  }
28105
28180
  /** Answer one MCP request by passing it on, enforcing what this computer may not do. */
28106
28181
  async handle(request, response) {
28107
- const origin = await this.#origin;
28182
+ const served = await this.#served;
28108
28183
  const chunks = [];
28109
28184
  let size = 0;
28110
28185
  for await (const chunk of request) {
@@ -28148,7 +28223,7 @@ var Browser = class {
28148
28223
  const session = headers["mcp-session-id"] ?? "";
28149
28224
  if (request.method === "DELETE") this.gate?.sessionEnded(session);
28150
28225
  if (tool && this.gate?.takeAnsweredChooser(session) && tool !== "browser_file_upload")
28151
- await fetch(`${origin}/mcp`, {
28226
+ await served(MCP, {
28152
28227
  method: "POST",
28153
28228
  headers,
28154
28229
  body: JSON.stringify({
@@ -28161,7 +28236,7 @@ var Browser = class {
28161
28236
  });
28162
28237
  if (tool === "browser_file_upload") this.gate?.agentAnsweredChooser();
28163
28238
  if (message) headers["content-type"] = "application/json";
28164
- const upstream = await fetch(`${origin}/mcp`, {
28239
+ const upstream = await served(MCP, {
28165
28240
  method: request.method,
28166
28241
  headers,
28167
28242
  ...message ? { body: JSON.stringify(message) } : {}
@@ -28176,6 +28251,7 @@ var Browser = class {
28176
28251
  async stop() {
28177
28252
  this.#stopping = true;
28178
28253
  await Promise.all([this.#mcp, this.#host].map((child) => child ? end(child) : void 0));
28254
+ if (this.#socketFolder) rmSync3(this.#socketFolder, { recursive: true, force: true });
28179
28255
  }
28180
28256
  };
28181
28257
 
@@ -28188,7 +28264,7 @@ import { join as join7 } from "node:path";
28188
28264
  import { chmodSync as chmodSync2, existsSync, statSync as statSync2 } from "node:fs";
28189
28265
  import { createRequire as createRequire3 } from "node:module";
28190
28266
  import { hostname as hostname3 } from "node:os";
28191
- import { basename as basename2, dirname as dirname3, join as join6 } from "node:path";
28267
+ import { basename as basename2, dirname as dirname4, join as join6 } from "node:path";
28192
28268
 
28193
28269
  // node_modules/@agentorg/exec/dist/lib.js
28194
28270
  import { realpathSync } from "node:fs";
@@ -28198,7 +28274,7 @@ import { Http2ServerRequest, constants } from "node:http2";
28198
28274
  import { Readable as Readable2 } from "node:stream";
28199
28275
  import { randomBytes as randomBytes3 } from "node:crypto";
28200
28276
  import { lstat as lstat2, open as open3, readdir as readdir2, rename as rename2, rm as rm2, stat as stat2 } from "node:fs/promises";
28201
- import { dirname as dirname2, join as join3 } from "node:path";
28277
+ import { dirname as dirname2, join as join32 } from "node:path";
28202
28278
  import { randomBytes as randomBytes2 } from "node:crypto";
28203
28279
  import {
28204
28280
  chmod,
@@ -28212,7 +28288,7 @@ import {
28212
28288
  stat,
28213
28289
  writeFile
28214
28290
  } from "node:fs/promises";
28215
- import { basename, dirname, join as join4, resolve, sep as sep2 } from "node:path";
28291
+ import { basename, dirname as dirname3, join as join4, resolve, sep as sep2 } from "node:path";
28216
28292
  import { spawn as spawn2 } from "node:child_process";
28217
28293
  import { randomBytes } from "node:crypto";
28218
28294
  import { StringDecoder } from "node:string_decoder";
@@ -66245,7 +66321,7 @@ async function inside(root, path) {
66245
66321
  () => false
66246
66322
  ))
66247
66323
  throw refused2();
66248
- const above = dirname(at);
66324
+ const above = dirname3(at);
66249
66325
  if (above === at) throw refused2();
66250
66326
  return join4(await real(above), basename(at));
66251
66327
  }
@@ -66333,7 +66409,7 @@ ${cut(shown.join("\n"), MAX_READ_CHARS)}`;
66333
66409
  after = before.split(input2.old_text).join(input2.new_text);
66334
66410
  }
66335
66411
  if (after === before) throw new ExecRefused("That edit changes nothing.");
66336
- const temporary = join4(dirname(path), `.agentexec-${randomBytes2(6).toString("hex")}`);
66412
+ const temporary = join4(dirname3(path), `.agentexec-${randomBytes2(6).toString("hex")}`);
66337
66413
  try {
66338
66414
  await writeFile(temporary, after, found ? { mode: found.mode } : {});
66339
66415
  if (found) await chmod(temporary, found.mode & 511);
@@ -66472,7 +66548,7 @@ function createHostFiles(root, level) {
66472
66548
  found.sort(foldersFirst);
66473
66549
  const entries = await Promise.all(
66474
66550
  found.slice(0, MAX_ENTRIES).map(async (entry) => {
66475
- const own22 = await lstat2(join3(folder, entry.name)).catch(() => void 0);
66551
+ const own22 = await lstat2(join32(folder, entry.name)).catch(() => void 0);
66476
66552
  return own22 && {
66477
66553
  name: entry.name,
66478
66554
  kind: entry.isDirectory() ? "folder" : entry.isSymbolicLink() ? "link" : "file",
@@ -66519,7 +66595,7 @@ function createHostFiles(root, level) {
66519
66595
  });
66520
66596
  if (before?.isDirectory()) throw new ExecRefused(`${path} is a folder.`);
66521
66597
  await stat2(dirname2(target));
66522
- const temporary = join3(dirname2(target), `.agentexec-${randomBytes3(6).toString("hex")}`);
66598
+ const temporary = join32(dirname2(target), `.agentexec-${randomBytes3(6).toString("hex")}`);
66523
66599
  const file2 = await open3(temporary, "wx");
66524
66600
  const discard = async () => {
66525
66601
  await file2.close().catch(() => {
@@ -67773,7 +67849,7 @@ var loadPty = () => {
67773
67849
  } catch {
67774
67850
  return fails("node-pty isn't installed with this service.");
67775
67851
  }
67776
- const root = dirname3(require3.resolve("node-pty/package.json"));
67852
+ const root = dirname4(require3.resolve("node-pty/package.json"));
67777
67853
  for (const helper of [
67778
67854
  join6(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
67779
67855
  join6(root, "build", "Release", "spawn-helper")
@@ -68203,7 +68279,7 @@ async function forward(url3, authorization, request, response) {
68203
68279
  }
68204
68280
 
68205
68281
  // src/handlers.ts
68206
- var VERSION2 = "0.0.15";
68282
+ var VERSION2 = "0.0.16";
68207
68283
  function createHandlers(computers) {
68208
68284
  return {
68209
68285
  getInfo: async () => ({
@@ -69101,7 +69177,7 @@ var Computer = class _Computer {
69101
69177
  };
69102
69178
 
69103
69179
  // src/service.ts
69104
- import { readFileSync as readFileSync6, rmSync as rmSync3 } from "node:fs";
69180
+ import { readFileSync as readFileSync6, rmSync as rmSync5 } from "node:fs";
69105
69181
  import {
69106
69182
  createServer as createServer5,
69107
69183
  STATUS_CODES as STATUS_CODES3
@@ -84921,7 +84997,7 @@ var NodeStreamableHTTPServerTransport2 = class {
84921
84997
 
84922
84998
  // src/computers.ts
84923
84999
  import { randomBytes as randomBytes5 } from "node:crypto";
84924
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
85000
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync4 } from "node:fs";
84925
85001
  import { isAbsolute, join as join10, resolve as resolve4 } from "node:path";
84926
85002
  var READ = ["about", "read", "list", "search"];
84927
85003
  var FILES = [...READ, "edit"];
@@ -85026,7 +85102,7 @@ var Computers = class {
85026
85102
  this.#stored.delete(id);
85027
85103
  await this.#runtimes[stored.isolation].remove(id).catch(() => {
85028
85104
  });
85029
- rmSync2(this.#folder(id), { recursive: true, force: true });
85105
+ rmSync4(this.#folder(id), { recursive: true, force: true });
85030
85106
  throw refusalOf(error62);
85031
85107
  }
85032
85108
  this.#save();
@@ -85100,7 +85176,7 @@ var Computers = class {
85100
85176
  }
85101
85177
  this.#stored.delete(id);
85102
85178
  this.#save();
85103
- rmSync2(this.#folder(id), { recursive: true, force: true });
85179
+ rmSync4(this.#folder(id), { recursive: true, force: true });
85104
85180
  }
85105
85181
  logs(id) {
85106
85182
  this.#found(id);
@@ -85271,7 +85347,7 @@ var OPENAPI = {
85271
85347
  "openapi": "3.1.0",
85272
85348
  "info": {
85273
85349
  "title": "AgentComputer",
85274
- "version": "0.0.15",
85350
+ "version": "0.0.16",
85275
85351
  "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.",
85276
85352
  "license": {
85277
85353
  "name": "Apache-2.0",
@@ -88415,7 +88491,7 @@ ${JSON.stringify({ code: status, message })}
88415
88491
  const scheme = options.tls ? "https" : "http";
88416
88492
  base = options.publicUrl?.replace(/\/+$/, "") ?? `${scheme}://${bound.family === "IPv6" ? `[${bound.address}]` : bound.address}:${bound.port}`;
88417
88493
  const record3 = recordOf(data);
88418
- rmSync3(record3, { force: true });
88494
+ rmSync5(record3, { force: true });
88419
88495
  await computers.resume();
88420
88496
  writePrivate(
88421
88497
  record3,
@@ -88431,7 +88507,7 @@ ${JSON.stringify({ code: status, message })}
88431
88507
  process.stderr.write(`${String(error62)}
88432
88508
  `);
88433
88509
  });
88434
- rmSync3(record3, { force: true });
88510
+ rmSync5(record3, { force: true });
88435
88511
  process.exit(0);
88436
88512
  };
88437
88513
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => void stop());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentorg/computer",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Computers for agents, as a service: a terminal, files and a browser over MCP",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {