humanish 0.28.0 → 0.30.0

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.
@@ -9,16 +9,28 @@ import type { E2BDesktopSandbox } from "./e2b-desktop-launch.js";
9
9
  * unlikely to collide with a subject app; override via config if it does. */
10
10
  export declare const DEFAULT_SANDBOX_CATCH_PORT = 8025;
11
11
  /**
12
- * The self-contained in-sandbox capture server (a plain node ESM string runs on the sandbox's own
13
- * node, imports nothing from humanish). It is DELIBERATELY dumb: it records each POST verbatim as an
14
- * NDJSON line `{t, path, body}` and returns a plausible provider success all normalization/profile
15
- * parsing happens host-side on the drained lines, so the typed, tested profiles stay in one place.
16
- * argv: <port> <deliveriesFile>.
12
+ * The self-contained in-sandbox capture server a plain **python3** script (stdlib only), because the
13
+ * stock E2B desktop template ships python3 but NOT node, and the co-located catcher must run in a
14
+ * runtime the sandbox guarantees (the precedented choice: LocalStack is a python catcher the app points
15
+ * at; you pick the runtime the environment has). It runs on the sandbox's own python3, imports nothing
16
+ * from humanish. DELIBERATELY dumb: it records each POST verbatim as an NDJSON line `{t, path, body}`
17
+ * and returns a plausible provider success — all normalization/profile parsing happens host-side on the
18
+ * drained lines, so the typed, tested profiles stay in one place. It also serves the host-rendered inbox
19
+ * surface statically at /inbox + /api/inbox (with a script-forbidding CSP). argv: <port> <deliveriesFile>
20
+ * <servedDir> [inboxPort]. The capture listener binds 127.0.0.1 (loopback) — the app under test reaches
21
+ * it in-sandbox, nothing on the internet can inject a fake send. When an [inboxPort] is given (the
22
+ * shared-world route, where the persona lives in a DIFFERENT sandbox), it ALSO starts a READ-ONLY inbox
23
+ * listener on 0.0.0.0:<inboxPort> so getHost can proxy the persona's inbox reads to it; that listener
24
+ * serves GET only (POST → 405). The CUA same-sandbox route omits it and stays loopback-only.
17
25
  */
18
- export declare const SANDBOX_CATCH_SCRIPT: string;
26
+ export declare const SANDBOX_CATCH_SCRIPT = "import json\nimport os\nimport random\nimport sys\nimport threading\nimport time\nfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\nfrom urllib.parse import unquote\n\nPORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8025\nOUT_FILE = sys.argv[2] if len(sys.argv) > 2 else \"/tmp/humanish-comms/deliveries.ndjson\"\nSERVED_DIR = sys.argv[3] if len(sys.argv) > 3 else (os.path.dirname(OUT_FILE) + \"/surface\")\nINBOX_PORT = int(sys.argv[4]) if len(sys.argv) > 4 else 0\ntry:\n os.makedirs(os.path.dirname(OUT_FILE), exist_ok=True)\nexcept Exception:\n pass\nMAX_BODY = 5 * 1024 * 1024\nCSP = \"default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'none'; frame-src 'none'; img-src * data:; style-src 'unsafe-inline'; font-src * data:\"\n\n\ndef message_id():\n return \"humanish-catch-\" + format(int(time.time() * 1000), \"x\") + format(random.randrange(16 ** 8), \"08x\")\n\n\nclass BaseHandler(BaseHTTPRequestHandler):\n def log_message(self, *args):\n return\n\n def _json(self, status, obj, extra_headers=None):\n payload = json.dumps(obj).encode(\"utf-8\")\n self.send_response(status)\n self.send_header(\"content-type\", \"application/json; charset=utf-8\")\n for key, value in (extra_headers or {}).items():\n self.send_header(key, value)\n self.end_headers()\n self.wfile.write(payload)\n\n def do_GET(self):\n path = self.path.split(\"?\")[0]\n if path == \"/\" or path == \"/health\":\n self._json(200, {\"ok\": True, \"service\": \"humanish-comms-catch\"})\n return\n if path == \"/inbox\" or path.startswith(\"/inbox/\") or path == \"/api/inbox\" or path.startswith(\"/api/inbox/\"):\n rel = unquote(path)\n if \"..\" in rel or chr(0) in rel:\n self.send_response(400)\n self.end_headers()\n return\n data = None\n for candidate in (SERVED_DIR + rel, SERVED_DIR + rel + \"/index\"):\n try:\n with open(candidate, \"rb\") as handle:\n data = handle.read()\n break\n except Exception:\n data = None\n if data is None:\n self.send_response(404)\n self.send_header(\"content-type\", \"text/html; charset=utf-8\")\n self.end_headers()\n self.wfile.write(b\"<p>message not found</p>\")\n return\n is_api = rel.startswith(\"/api/\")\n self.send_response(200)\n self.send_header(\"content-type\", \"application/json; charset=utf-8\" if is_api else \"text/html; charset=utf-8\")\n if not is_api:\n self.send_header(\"content-security-policy\", CSP)\n self.end_headers()\n self.wfile.write(data)\n return\n self._json(404, {\"error\": \"not found\"})\n\n\nclass CaptureHandler(BaseHandler):\n def do_POST(self):\n path = self.path.split(\"?\")[0]\n try:\n length = int(self.headers.get(\"content-length\") or 0)\n except Exception:\n length = 0\n if length > MAX_BODY:\n self.send_response(413)\n self.end_headers()\n return\n body = self.rfile.read(length).decode(\"utf-8\", \"replace\") if length > 0 else \"\"\n try:\n with open(OUT_FILE, \"a\", encoding=\"utf-8\") as handle:\n print(json.dumps({\"t\": int(time.time() * 1000), \"path\": path, \"body\": body}), file=handle)\n except Exception:\n pass\n mid = message_id()\n if path == \"/v3/mail/send\":\n self.send_response(202)\n self.send_header(\"x-message-id\", mid)\n self.end_headers()\n elif path.endswith(\"/batch\"):\n self._json(200, {\"data\": [{\"id\": mid}]})\n else:\n self._json(200, {\"id\": mid})\n\n\nclass ReadOnlyHandler(BaseHandler):\n def do_POST(self):\n self.send_response(405)\n self.end_headers()\n\n\n# Optional read-only inbox listener on 0.0.0.0 (getHost-reachable from a DIFFERENT sandbox on the\n# shared-world route). Serves GET /inbox + /api/inbox + /health only; POST capture stays on the\n# 127.0.0.1 listener so nothing on the internet can inject a fake captured send. Started only when a\n# distinct inbox port is provided (the CUA same-sandbox route omits it and stays loopback-only).\nif INBOX_PORT and INBOX_PORT != PORT:\n threading.Thread(target=lambda: ThreadingHTTPServer((\"0.0.0.0\", INBOX_PORT), ReadOnlyHandler).serve_forever(), daemon=True).start()\n\nThreadingHTTPServer((\"127.0.0.1\", PORT), CaptureHandler).serve_forever()\n";
19
27
  export interface DeployCommsCatchOptions {
20
28
  /** Fixed loopback port the catch listens on (default 8025). Must be free inside the sandbox. */
21
29
  port?: number;
30
+ /** Optional SECOND fixed port for a READ-ONLY inbox listener bound to 0.0.0.0, so a persona in a
31
+ * DIFFERENT sandbox can reach the inbox surface via getHost (the shared-world route). Omit on the
32
+ * CUA same-sandbox route (loopback is enough). Must differ from `port` and be free in the sandbox. */
33
+ inboxPort?: number;
22
34
  /** In-sandbox working dir for the script + NDJSON (default /tmp/humanish-comms). */
23
35
  dir?: string;
24
36
  /** Detached-process name ([a-z0-9-]); default "comms-catch". */
@@ -36,6 +48,9 @@ export interface DeployedCommsCatch {
36
48
  /** In-sandbox dir the HOST renders the persona-facing inbox-surface files into (via writeInboxSurface);
37
49
  * the catch serves them at /inbox and /api/inbox. */
38
50
  surfaceDir: string;
51
+ /** The 0.0.0.0 read-only inbox port, when one was requested — getHost-expose THIS to give a
52
+ * different-sandbox persona a reachable inbox URL. Absent on the loopback-only (CUA) route. */
53
+ inboxPort?: number;
39
54
  /** Whether the catch's /health returned OUR service marker within the readiness budget. Callers MUST
40
55
  * treat `ready === false` as fatal (do not inject baseUrl into a dead catch — the app's sends would
41
56
  * silently fail with nothing captured). */
@@ -106,27 +121,32 @@ export declare function collectCommsThread(args: {
106
121
  export declare function writeInboxSurface(desktop: E2BDesktopSandbox, surfaceDir: string, messages: CommsMessage[], options?: InboxRenderOptions & {
107
122
  requestTimeoutMs?: number;
108
123
  }): Promise<number>;
124
+ /** A declared inbox recipient the surface renders for (lane + the literal address the app sends to). */
125
+ export interface InboxSurfaceRecipient {
126
+ lane: string;
127
+ address: string;
128
+ }
109
129
  /**
110
- * One mid-run inbox-surface refresh cycle: drain the catch INCREMENTALLY from `cursor` into the
111
- * persistent surface `channel`, poll the provisioned `inboxes`, and (re)render the surface so the
112
- * persona sees new mail while the session is live. Returns the advanced cursor + whether it rendered.
130
+ * One mid-run inbox-surface refresh cycle: FULL rebuild from the append-only NDJSON (drain from cursor 0)
131
+ * into a FRESH FakeInbox each call, provisioning the declared `recipients`, then (re)render the surface
132
+ * so the persona sees new mail while the session is live. Returns the total captured-send `count` + whether
133
+ * it rendered.
113
134
  *
114
- * MUST use a channel dedicated to the surface NOT the teardown evidence channel: the teardown
115
- * collectCommsThread drains the same append-only NDJSON from cursor 0 into its OWN fresh channel, so
116
- * feeding both an incremental and a cursor-0 drain into one channel would route every send twice
117
- * (FakeInbox mints fresh ids each time, so dedup-by-id can't catch it) and double-count the evidence.
118
- * Two channels + two cursors are independent readers of the same file no evidence is lost or altered.
119
- * Skips the (N-file) render when no new sends arrived this tick, so idle ticks are cheap.
135
+ * The full rebuild is deliberate it is IDEMPOTENT and RETRY-SAFE: a transient writeInboxSurface failure
136
+ * PROPAGATES (the caller retries next tick without advancing its `sinceCount`), and because each rebuild
137
+ * starts from a clean channel, a send is never routed twice, so the persona never sees duplicate emails.
138
+ * Pass `sinceCount` (the last SUCCESSFULLY-rendered send count) to skip the (N-file) render when nothing
139
+ * new has arrived. This is independent of the teardown collectCommsThread drain (its own fresh channel,
140
+ * also cursor 0) no evidence is lost or altered. The NDJSON is small for a run, so re-reading it is cheap.
120
141
  */
121
142
  export declare function refreshInboxSurface(args: {
122
143
  desktop: E2BDesktopSandbox;
123
144
  deployed: Pick<DeployedCommsCatch, "deliveriesPath" | "surfaceDir">;
124
- channel: CommsChannel;
125
- inboxes: CommsAddress[];
126
- cursor: number;
145
+ recipients: InboxSurfaceRecipient[];
146
+ sinceCount?: number;
127
147
  originMap?: InboxRenderOptions["originMap"];
128
148
  requestTimeoutMs?: number;
129
149
  }): Promise<{
130
- cursor: number;
150
+ count: number;
131
151
  rendered: boolean;
132
152
  }>;
@@ -6,6 +6,7 @@
6
6
  // each poll `cat` its append-only NDJSON of captured sends back to the host, where the real profiles
7
7
  // parse them and route into the CommsChannel. A FIXED loopback port is chosen up front so the app's
8
8
  // injected base-URL env (`http://127.0.0.1:<port>`) is known before the sandbox is created.
9
+ import { FakeInbox } from "./comms-fake-inbox.js";
9
10
  import { buildCommsThreadArtifact } from "./comms-evidence.js";
10
11
  import { buildInboxSurface } from "./comms-inbox.js";
11
12
  import { DEFAULT_EMAIL_PROFILES } from "./comms-email-catch.js";
@@ -20,53 +21,137 @@ function shq(value) {
20
21
  return `'${value.replaceAll("'", "'\\''")}'`;
21
22
  }
22
23
  /**
23
- * The self-contained in-sandbox capture server (a plain node ESM string runs on the sandbox's own
24
- * node, imports nothing from humanish). It is DELIBERATELY dumb: it records each POST verbatim as an
25
- * NDJSON line `{t, path, body}` and returns a plausible provider success all normalization/profile
26
- * parsing happens host-side on the drained lines, so the typed, tested profiles stay in one place.
27
- * argv: <port> <deliveriesFile>.
24
+ * The self-contained in-sandbox capture server a plain **python3** script (stdlib only), because the
25
+ * stock E2B desktop template ships python3 but NOT node, and the co-located catcher must run in a
26
+ * runtime the sandbox guarantees (the precedented choice: LocalStack is a python catcher the app points
27
+ * at; you pick the runtime the environment has). It runs on the sandbox's own python3, imports nothing
28
+ * from humanish. DELIBERATELY dumb: it records each POST verbatim as an NDJSON line `{t, path, body}`
29
+ * and returns a plausible provider success — all normalization/profile parsing happens host-side on the
30
+ * drained lines, so the typed, tested profiles stay in one place. It also serves the host-rendered inbox
31
+ * surface statically at /inbox + /api/inbox (with a script-forbidding CSP). argv: <port> <deliveriesFile>
32
+ * <servedDir> [inboxPort]. The capture listener binds 127.0.0.1 (loopback) — the app under test reaches
33
+ * it in-sandbox, nothing on the internet can inject a fake send. When an [inboxPort] is given (the
34
+ * shared-world route, where the persona lives in a DIFFERENT sandbox), it ALSO starts a READ-ONLY inbox
35
+ * listener on 0.0.0.0:<inboxPort> so getHost can proxy the persona's inbox reads to it; that listener
36
+ * serves GET only (POST → 405). The CUA same-sandbox route omits it and stays loopback-only.
28
37
  */
29
- export const SANDBOX_CATCH_SCRIPT = [
30
- 'import { createServer } from "node:http";',
31
- 'import { appendFileSync, mkdirSync, readFileSync } from "node:fs";',
32
- 'import { dirname } from "node:path";',
33
- 'const port = Number(process.argv[2] || 8025);',
34
- 'const outFile = process.argv[3] || "/tmp/humanish-comms/deliveries.ndjson";',
35
- // argv[4] is the dir the HOST renders the inbox-surface files into (host-side typed rendering; this
36
- // server only serves them statically), defaulting to a `surface` sibling of the deliveries file.
37
- 'const servedDir = process.argv[4] || (dirname(outFile) + "/surface");',
38
- 'try { mkdirSync(dirname(outFile), { recursive: true }); } catch {}',
39
- 'const MAX = 5 * 1024 * 1024;',
40
- 'createServer((req, res) => {',
41
- ' const path = (req.url || "/").split("?")[0];',
42
- ' if (req.method === "GET" && (path === "/" || path === "/health")) { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true, service: "humanish-comms-catch" })); return; }',
43
- ' if (req.method === "GET" && (path === "/inbox" || path.indexOf("/inbox/") === 0 || path === "/api/inbox" || path.indexOf("/api/inbox/") === 0)) {',
44
- ' let rel; try { rel = decodeURIComponent(path); } catch { res.writeHead(400); res.end(); return; }',
45
- ' if (rel.indexOf("..") !== -1 || rel.indexOf("\\0") !== -1) { res.writeHead(400); res.end(); return; }',
46
- ' let data = null;',
47
- ' try { data = readFileSync(servedDir + rel); } catch { try { data = readFileSync(servedDir + rel + "/index"); } catch { data = null; } }',
48
- ' if (data === null) { res.writeHead(404, { "content-type": "text/html; charset=utf-8" }); res.end("<p>message not found</p>"); return; }',
49
- ' const isApi = rel.indexOf("/api/") === 0;',
50
- ' const headers = { "content-type": isApi ? "application/json; charset=utf-8" : "text/html; charset=utf-8" };',
51
- // The load-bearing XSS protection: a browser-enforced CSP forbidding all script on the surface page,
52
- // so the app-authored (untrusted) email HTML we render cannot run script or reroot navigation.
53
- ' if (!isApi) { headers["content-security-policy"] = "default-src \'self\'; script-src \'none\'; object-src \'none\'; base-uri \'none\'; frame-src \'none\'; img-src * data:; style-src \'unsafe-inline\'; font-src * data:"; }',
54
- ' res.writeHead(200, headers); res.end(data); return;',
55
- ' }',
56
- ' if (req.method !== "POST") { res.writeHead(404, { "content-type": "application/json" }); res.end(JSON.stringify({ error: "not found" })); return; }',
57
- ' let size = 0; const chunks = [];',
58
- ' req.on("data", (c) => { size += c.length; if (size > MAX) { req.destroy(); } else { chunks.push(c); } });',
59
- ' req.on("end", () => {',
60
- ' const body = Buffer.concat(chunks).toString("utf8");',
61
- ' try { appendFileSync(outFile, JSON.stringify({ t: Date.now(), path, body }) + "\\n"); } catch {}',
62
- ' const id = "humanish-catch-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);',
63
- ' if (path === "/v3/mail/send") { res.writeHead(202, { "x-message-id": id }); res.end(); }',
64
- ' else if (path.endsWith("/batch")) { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ data: [{ id }] })); }',
65
- ' else { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ id })); }',
66
- ' });',
67
- ' req.on("error", () => { try { res.writeHead(400); res.end(); } catch {} });',
68
- '}).listen(port, "127.0.0.1");'
69
- ].join("\n");
38
+ export const SANDBOX_CATCH_SCRIPT = `import json
39
+ import os
40
+ import random
41
+ import sys
42
+ import threading
43
+ import time
44
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
45
+ from urllib.parse import unquote
46
+
47
+ PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8025
48
+ OUT_FILE = sys.argv[2] if len(sys.argv) > 2 else "/tmp/humanish-comms/deliveries.ndjson"
49
+ SERVED_DIR = sys.argv[3] if len(sys.argv) > 3 else (os.path.dirname(OUT_FILE) + "/surface")
50
+ INBOX_PORT = int(sys.argv[4]) if len(sys.argv) > 4 else 0
51
+ try:
52
+ os.makedirs(os.path.dirname(OUT_FILE), exist_ok=True)
53
+ except Exception:
54
+ pass
55
+ MAX_BODY = 5 * 1024 * 1024
56
+ CSP = "default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'none'; frame-src 'none'; img-src * data:; style-src 'unsafe-inline'; font-src * data:"
57
+
58
+
59
+ def message_id():
60
+ return "humanish-catch-" + format(int(time.time() * 1000), "x") + format(random.randrange(16 ** 8), "08x")
61
+
62
+
63
+ class BaseHandler(BaseHTTPRequestHandler):
64
+ def log_message(self, *args):
65
+ return
66
+
67
+ def _json(self, status, obj, extra_headers=None):
68
+ payload = json.dumps(obj).encode("utf-8")
69
+ self.send_response(status)
70
+ self.send_header("content-type", "application/json; charset=utf-8")
71
+ for key, value in (extra_headers or {}).items():
72
+ self.send_header(key, value)
73
+ self.end_headers()
74
+ self.wfile.write(payload)
75
+
76
+ def do_GET(self):
77
+ path = self.path.split("?")[0]
78
+ if path == "/" or path == "/health":
79
+ self._json(200, {"ok": True, "service": "humanish-comms-catch"})
80
+ return
81
+ if path == "/inbox" or path.startswith("/inbox/") or path == "/api/inbox" or path.startswith("/api/inbox/"):
82
+ rel = unquote(path)
83
+ if ".." in rel or chr(0) in rel:
84
+ self.send_response(400)
85
+ self.end_headers()
86
+ return
87
+ data = None
88
+ for candidate in (SERVED_DIR + rel, SERVED_DIR + rel + "/index"):
89
+ try:
90
+ with open(candidate, "rb") as handle:
91
+ data = handle.read()
92
+ break
93
+ except Exception:
94
+ data = None
95
+ if data is None:
96
+ self.send_response(404)
97
+ self.send_header("content-type", "text/html; charset=utf-8")
98
+ self.end_headers()
99
+ self.wfile.write(b"<p>message not found</p>")
100
+ return
101
+ is_api = rel.startswith("/api/")
102
+ self.send_response(200)
103
+ self.send_header("content-type", "application/json; charset=utf-8" if is_api else "text/html; charset=utf-8")
104
+ if not is_api:
105
+ self.send_header("content-security-policy", CSP)
106
+ self.end_headers()
107
+ self.wfile.write(data)
108
+ return
109
+ self._json(404, {"error": "not found"})
110
+
111
+
112
+ class CaptureHandler(BaseHandler):
113
+ def do_POST(self):
114
+ path = self.path.split("?")[0]
115
+ try:
116
+ length = int(self.headers.get("content-length") or 0)
117
+ except Exception:
118
+ length = 0
119
+ if length > MAX_BODY:
120
+ self.send_response(413)
121
+ self.end_headers()
122
+ return
123
+ body = self.rfile.read(length).decode("utf-8", "replace") if length > 0 else ""
124
+ try:
125
+ with open(OUT_FILE, "a", encoding="utf-8") as handle:
126
+ print(json.dumps({"t": int(time.time() * 1000), "path": path, "body": body}), file=handle)
127
+ except Exception:
128
+ pass
129
+ mid = message_id()
130
+ if path == "/v3/mail/send":
131
+ self.send_response(202)
132
+ self.send_header("x-message-id", mid)
133
+ self.end_headers()
134
+ elif path.endswith("/batch"):
135
+ self._json(200, {"data": [{"id": mid}]})
136
+ else:
137
+ self._json(200, {"id": mid})
138
+
139
+
140
+ class ReadOnlyHandler(BaseHandler):
141
+ def do_POST(self):
142
+ self.send_response(405)
143
+ self.end_headers()
144
+
145
+
146
+ # Optional read-only inbox listener on 0.0.0.0 (getHost-reachable from a DIFFERENT sandbox on the
147
+ # shared-world route). Serves GET /inbox + /api/inbox + /health only; POST capture stays on the
148
+ # 127.0.0.1 listener so nothing on the internet can inject a fake captured send. Started only when a
149
+ # distinct inbox port is provided (the CUA same-sandbox route omits it and stays loopback-only).
150
+ if INBOX_PORT and INBOX_PORT != PORT:
151
+ threading.Thread(target=lambda: ThreadingHTTPServer(("0.0.0.0", INBOX_PORT), ReadOnlyHandler).serve_forever(), daemon=True).start()
152
+
153
+ ThreadingHTTPServer(("127.0.0.1", PORT), CaptureHandler).serve_forever()
154
+ `;
70
155
  /** Readiness probe that asserts OUR service marker in the /health body (not merely any 2xx) — so a
71
156
  * process squatting on the fixed port cannot produce a false "ready" while the app's sends bypass us. */
72
157
  async function catchHealthy(desktop, port, options) {
@@ -95,25 +180,36 @@ export async function deployCommsCatch(desktop, options = {}) {
95
180
  if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
96
181
  throw new Error(`deployCommsCatch: invalid port ${JSON.stringify(options.port)}`);
97
182
  }
183
+ const inboxPort = options.inboxPort === undefined ? undefined : Math.trunc(Number(options.inboxPort));
184
+ if (inboxPort !== undefined && (!Number.isInteger(inboxPort) || inboxPort <= 0 || inboxPort > 65_535 || inboxPort === port)) {
185
+ throw new Error(`deployCommsCatch: invalid inboxPort ${JSON.stringify(options.inboxPort)}`);
186
+ }
98
187
  const dir = options.dir ?? DEFAULT_CATCH_DIR;
99
188
  const name = options.name ?? "comms-catch";
100
189
  const requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
101
- const scriptPath = `${dir}/catch.mjs`;
190
+ const scriptPath = `${dir}/catch.py`;
102
191
  const deliveriesPath = `${dir}/deliveries.ndjson`;
103
192
  const surfaceDir = `${dir}/surface`;
104
193
  await desktop.commands.run(`mkdir -p ${shq(dir)} ${shq(surfaceDir)}`, { requestTimeoutMs });
105
194
  await desktop.files.write(scriptPath, SANDBOX_CATCH_SCRIPT);
106
195
  await startDetachedProcess(desktop, {
107
196
  name,
108
- command: `node ${shq(scriptPath)} ${port} ${shq(deliveriesPath)} ${shq(surfaceDir)}`,
197
+ command: `python3 ${shq(scriptPath)} ${port} ${shq(deliveriesPath)} ${shq(surfaceDir)}${inboxPort === undefined ? "" : ` ${inboxPort}`}`,
109
198
  requestTimeoutMs
110
199
  });
111
- const ready = await catchHealthy(desktop, port, {
112
- timeoutMs: options.readyTimeoutMs ?? 15_000,
113
- requestTimeoutMs,
114
- ...(options.timers ?? {})
115
- });
116
- return { port, baseUrl: `http://127.0.0.1:${port}`, deliveriesPath, surfaceDir, ready };
200
+ const probe = { timeoutMs: options.readyTimeoutMs ?? 15_000, requestTimeoutMs, ...(options.timers ?? {}) };
201
+ const ready = await catchHealthy(desktop, port, probe);
202
+ // Confirm the read-only inbox listener bound too (loopback-reachable at its own port), when requested —
203
+ // else a getHost-exposed inbox would 502. Fail closed by folding it into `ready`.
204
+ const inboxReady = inboxPort === undefined ? true : await catchHealthy(desktop, inboxPort, probe);
205
+ return {
206
+ port,
207
+ baseUrl: `http://127.0.0.1:${port}`,
208
+ deliveriesPath,
209
+ surfaceDir,
210
+ ...(inboxPort === undefined ? {} : { inboxPort }),
211
+ ready: ready && inboxReady
212
+ };
117
213
  }
118
214
  /**
119
215
  * Drain new captured sends from the in-sandbox NDJSON since `cursor` (a line count). Returns the fresh
@@ -227,26 +323,33 @@ export async function writeInboxSurface(desktop, surfaceDir, messages, options =
227
323
  return files.length;
228
324
  }
229
325
  /**
230
- * One mid-run inbox-surface refresh cycle: drain the catch INCREMENTALLY from `cursor` into the
231
- * persistent surface `channel`, poll the provisioned `inboxes`, and (re)render the surface so the
232
- * persona sees new mail while the session is live. Returns the advanced cursor + whether it rendered.
326
+ * One mid-run inbox-surface refresh cycle: FULL rebuild from the append-only NDJSON (drain from cursor 0)
327
+ * into a FRESH FakeInbox each call, provisioning the declared `recipients`, then (re)render the surface
328
+ * so the persona sees new mail while the session is live. Returns the total captured-send `count` + whether
329
+ * it rendered.
233
330
  *
234
- * MUST use a channel dedicated to the surface NOT the teardown evidence channel: the teardown
235
- * collectCommsThread drains the same append-only NDJSON from cursor 0 into its OWN fresh channel, so
236
- * feeding both an incremental and a cursor-0 drain into one channel would route every send twice
237
- * (FakeInbox mints fresh ids each time, so dedup-by-id can't catch it) and double-count the evidence.
238
- * Two channels + two cursors are independent readers of the same file no evidence is lost or altered.
239
- * Skips the (N-file) render when no new sends arrived this tick, so idle ticks are cheap.
331
+ * The full rebuild is deliberate it is IDEMPOTENT and RETRY-SAFE: a transient writeInboxSurface failure
332
+ * PROPAGATES (the caller retries next tick without advancing its `sinceCount`), and because each rebuild
333
+ * starts from a clean channel, a send is never routed twice, so the persona never sees duplicate emails.
334
+ * Pass `sinceCount` (the last SUCCESSFULLY-rendered send count) to skip the (N-file) render when nothing
335
+ * new has arrived. This is independent of the teardown collectCommsThread drain (its own fresh channel,
336
+ * also cursor 0) no evidence is lost or altered. The NDJSON is small for a run, so re-reading it is cheap.
240
337
  */
241
338
  export async function refreshInboxSurface(args) {
242
- const { sends, cursor } = await drainCommsCatch(args.desktop, args.deployed, args.cursor, args.requestTimeoutMs);
339
+ const { sends } = await drainCommsCatch(args.desktop, args.deployed, 0, args.requestTimeoutMs);
243
340
  if (sends.length === 0)
244
- return { cursor, rendered: false };
245
- await routeCapturedSends(sends, args.channel);
341
+ return { count: 0, rendered: false };
342
+ if (args.sinceCount !== undefined && sends.length <= args.sinceCount)
343
+ return { count: sends.length, rendered: false };
344
+ const channel = new FakeInbox();
345
+ const inboxes = [];
346
+ for (const recipient of args.recipients)
347
+ inboxes.push(await channel.provisionAddress(recipient.lane, recipient.address));
348
+ await routeCapturedSends(sends, channel);
246
349
  const seen = new Set();
247
350
  const messages = [];
248
- for (const inbox of args.inboxes) {
249
- for (const message of await args.channel.poll(inbox, 0)) {
351
+ for (const inbox of inboxes) {
352
+ for (const message of await channel.poll(inbox, 0)) {
250
353
  if (seen.has(message.id))
251
354
  continue;
252
355
  seen.add(message.id);
@@ -254,12 +357,12 @@ export async function refreshInboxSurface(args) {
254
357
  }
255
358
  }
256
359
  if (messages.length === 0)
257
- return { cursor, rendered: false };
360
+ return { count: sends.length, rendered: false };
258
361
  messages.sort((a, b) => a.deliveredAt - b.deliveredAt || a.id.localeCompare(b.id));
259
362
  await writeInboxSurface(args.desktop, args.deployed.surfaceDir, messages, {
260
363
  ...(args.originMap === undefined ? {} : { originMap: args.originMap }),
261
364
  ...(args.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: args.requestTimeoutMs })
262
365
  });
263
- return { cursor, rendered: true };
366
+ return { count: sends.length, rendered: true };
264
367
  }
265
368
  //# sourceMappingURL=comms-sandbox-catch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"comms-sandbox-catch.js","sourceRoot":"","sources":["../src/comms-sandbox-catch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"comms-sandbox-catch.js","sourceRoot":"","sources":["../src/comms-sandbox-catch.ts"],"names":[],"mappings":"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"}
@@ -39,9 +39,10 @@ import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-
39
39
  import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
40
40
  import { toErrorMessage } from "./command-failure.js";
41
41
  import { mapWithConcurrency } from "./concurrency.js";
42
- import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, provisionCloneSubject, provisionLocalTreeSubject, declaredScreenForRender, resolveLaneDevice, resolveSubjectState, runCuaLane } from "./cua-actor-lab.js";
43
- import { DEFAULT_SANDBOX_CATCH_PORT, collectCommsThread, deployCommsCatch } from "./comms-sandbox-catch.js";
42
+ import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, provisionCloneSubject, provisionLocalTreeSubject, declaredScreenForRender, laneHasInboxRecipient, resolveLaneDevice, resolveSubjectState, runCuaLane, withInboxMission } from "./cua-actor-lab.js";
43
+ import { DEFAULT_SANDBOX_CATCH_PORT, collectCommsThread, deployCommsCatch, refreshInboxSurface, writeInboxSurface } from "./comms-sandbox-catch.js";
44
44
  import { FakeInbox } from "./comms-fake-inbox.js";
45
+ import { buildOriginMap } from "./comms-inbox.js";
45
46
  import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
46
47
  import { concurrentSharedWorldValidationReason, externalPublicSharedWorldValidationReason } from "./lab-config.js";
47
48
  import { buildObserverData } from "./observer-data.js";
@@ -558,6 +559,13 @@ export async function runConcurrentSharedWorld(options) {
558
559
  let subjectSandboxId;
559
560
  let subjectKilled = false;
560
561
  let getHostUrl;
562
+ // Persona inbox SURFACE (#297 slice B, shared-world): the getHost-exposed inbox URL a persona (in a
563
+ // DIFFERENT sandbox) opens, the serve->getHost origin-rewrite map (REQUIRED here so the app's loopback
564
+ // verify links resolve to a reachable host), and the dedicated surface channel + render loop.
565
+ let commsInboxUrl;
566
+ let commsOriginMap = [];
567
+ let surfaceRenderedCount = 0;
568
+ let surfaceLoop;
561
569
  let runError;
562
570
  let snapshotIndex = 0;
563
571
  let liveObserver;
@@ -698,9 +706,11 @@ export async function runConcurrentSharedWorld(options) {
698
706
  // (injected into its env at create) resolves the moment it boots. Fail closed if the catch can't
699
707
  // stand up rather than let a comms-declared app silently send real mail to the internet.
700
708
  if (commsEmail && commsPort !== undefined) {
701
- deployedComms = await deployCommsCatch(subjectDesktop, { port: commsPort, requestTimeoutMs, timers });
709
+ // A SECOND (0.0.0.0) read-only inbox listener on commsPort+1 so the persona — which lives in a
710
+ // DIFFERENT sandbox here — can reach the inbox surface via getHost; capture stays loopback.
711
+ deployedComms = await deployCommsCatch(subjectDesktop, { port: commsPort, inboxPort: commsPort + 1, requestTimeoutMs, timers });
702
712
  if (!deployedComms.ready) {
703
- throw new Error(`comms email catch did not become ready on 127.0.0.1:${commsPort} in the subject sandbox`);
713
+ throw new Error(`comms email catch did not become ready in the subject sandbox (loopback capture ${commsPort} / inbox ${commsPort + 1})`);
704
714
  }
705
715
  }
706
716
  // Provision the ONE shared plane: clone + install/build + seed + serve on 0.0.0.0 + probe
@@ -748,6 +758,61 @@ export async function runConcurrentSharedWorld(options) {
748
758
  throw new Error("getHost returned a non-tokenless URL; refusing to persist a host URL that may carry a credential (invariant 1)");
749
759
  }
750
760
  getHostUrl = hostUrl;
761
+ // Persona inbox SURFACE (#297 slice B, shared-world): getHost-expose the read-only inbox listener so
762
+ // a persona in a DIFFERENT sandbox can open it; build the serve->getHost origin map (REQUIRED here —
763
+ // the app's loopback verify links must be rewritten to a reachable host); provision the surface
764
+ // channel; write the EMPTY inbox up front (so /inbox never 404s); and start a render loop that drains
765
+ // + re-renders on a cadence. The loop shares the prober's dispose signal (disposed together, before
766
+ // the teardown evidence drain), and uses a DEDICATED FakeInbox + cursor (independent of that drain).
767
+ if (commsEmail && deployedComms?.inboxPort !== undefined) {
768
+ const rawInboxHost = subjectDesktop.getHost(deployedComms.inboxPort);
769
+ const inboxHostUrl = /^https?:\/\//i.test(rawInboxHost) ? rawInboxHost : `https://${rawInboxHost}`;
770
+ if (!isTokenlessHost(inboxHostUrl)) {
771
+ throw new Error("getHost returned a non-tokenless URL for the comms inbox; refusing to advertise it (invariant 1)");
772
+ }
773
+ commsInboxUrl = `${inboxHostUrl}/inbox`;
774
+ commsOriginMap = buildOriginMap({
775
+ internalServeUrl: serve.url,
776
+ reachableBaseUrl: getHostUrl,
777
+ ...(commsEmail.linkOrigin === undefined ? {} : { linkOrigin: commsEmail.linkOrigin })
778
+ });
779
+ const surfaceRecipients = (commsEmail.recipients ?? [])
780
+ .filter((recipient) => recipient.address !== undefined)
781
+ .map((recipient) => ({ lane: recipient.lane, address: recipient.address }));
782
+ await writeInboxSurface(subjectDesktop, deployedComms.surfaceDir, [], { originMap: commsOriginMap, requestTimeoutMs });
783
+ const surfaceDeployed = deployedComms;
784
+ const surfaceCadenceMs = 2500;
785
+ surfaceLoop = (async () => {
786
+ // Full, idempotent rebuild each tick; surfaceRenderedCount advances only on a successful render,
787
+ // so a transient failure retries cleanly. Real timer (dispose-interruptible + cleared) — an
788
+ // unbounded loop must not busy-spin on the injected instant clock.
789
+ for (;;) {
790
+ try {
791
+ const refreshed = await refreshInboxSurface({
792
+ desktop: subjectDesktop,
793
+ deployed: surfaceDeployed,
794
+ recipients: surfaceRecipients,
795
+ sinceCount: surfaceRenderedCount,
796
+ originMap: commsOriginMap,
797
+ requestTimeoutMs
798
+ });
799
+ if (refreshed.rendered)
800
+ surfaceRenderedCount = refreshed.count;
801
+ }
802
+ catch {
803
+ // Never throw into the render loop; the teardown drain + by-id teardown must still run.
804
+ }
805
+ if (proberDisposed)
806
+ break;
807
+ await new Promise((resolve) => {
808
+ const timer = setTimeout(resolve, surfaceCadenceMs);
809
+ void disposeSignal.then(() => { clearTimeout(timer); resolve(); });
810
+ });
811
+ if (proberDisposed)
812
+ break;
813
+ }
814
+ })();
815
+ }
751
816
  // Baseline state snapshot, then start the background cadence prober.
752
817
  await proberSnapshot();
753
818
  if (options.onObserverReady) {
@@ -840,8 +905,13 @@ export async function runConcurrentSharedWorld(options) {
840
905
  };
841
906
  actorResults = await mapWithConcurrency(actorSpecs, Math.max(1, concurrency), async (spec, i) => {
842
907
  const route = resolveActorSeatUrl(getHostUrl, roles[i]?.entry);
908
+ // Tell this persona its (getHost-reachable) inbox URL — but only when comms is live AND this lane
909
+ // has a declared recipient it can actually receive mail into (else it would stall on an empty inbox).
910
+ const laneSpec = commsEmail && commsInboxUrl && laneHasInboxRecipient(commsEmail, spec.laneId)
911
+ ? withInboxMission(spec, commsInboxUrl)
912
+ : spec;
843
913
  const startedAt = now();
844
- const outcome = await runCuaLane(spec, { ...baseActorDeps, appUrl: route });
914
+ const outcome = await runCuaLane(laneSpec, { ...baseActorDeps, appUrl: route });
845
915
  const endedAt = now();
846
916
  return { spec, outcome, startedAt, endedAt, route };
847
917
  });
@@ -858,6 +928,11 @@ export async function runConcurrentSharedWorld(options) {
858
928
  if (proberLoop) {
859
929
  await proberLoop.catch(() => undefined);
860
930
  }
931
+ // Stop the inbox-surface render loop too (shares the prober's dispose signal), before the teardown
932
+ // evidence drain below — so the two in-sandbox reads never overlap and the surface state is final.
933
+ if (surfaceLoop) {
934
+ await surfaceLoop.catch(() => undefined);
935
+ }
861
936
  if (subjectDesktop && getHostUrl) {
862
937
  await proberSnapshot().catch(() => undefined);
863
938
  }