humanish 0.21.0 → 0.23.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.
- package/dist/comms-evidence.d.ts +27 -0
- package/dist/comms-evidence.js +31 -0
- package/dist/comms-evidence.js.map +1 -0
- package/dist/comms-sandbox-catch.d.ts +63 -0
- package/dist/comms-sandbox-catch.js +157 -0
- package/dist/comms-sandbox-catch.js.map +1 -0
- package/dist/cua-actor-lab.js +21 -2
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lab-config.d.ts +25 -0
- package/dist/lab-config.js +61 -0
- package/dist/lab-config.js.map +1 -1
- package/docs/contracts/schemas.md +1 -1
- package/docs/goals/current.md +1 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CommsMessage } from "./comms-types.js";
|
|
2
|
+
export declare const COMMS_THREAD_SCHEMA = "humanish.comms-thread.v1";
|
|
3
|
+
export interface CommsThreadEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
channel: CommsMessage["channel"];
|
|
6
|
+
/** sha256-16 of the raw sender address. */
|
|
7
|
+
fromDigest: string;
|
|
8
|
+
/** sha256-16 of each recipient inbox address. */
|
|
9
|
+
toDigests: string[];
|
|
10
|
+
/** sha256-16 of the subject (non-reversible for a PII subject; correlates identical subjects). */
|
|
11
|
+
subjectDigest?: string;
|
|
12
|
+
/** sha256-16 of each actionable link (high-entropy → non-reversible). */
|
|
13
|
+
linkDigests: string[];
|
|
14
|
+
/** COUNT ONLY — a short OTP's digest is reversible, so the code itself never lands here. */
|
|
15
|
+
codeCount: number;
|
|
16
|
+
sentAt: number;
|
|
17
|
+
deliveredAt: number;
|
|
18
|
+
}
|
|
19
|
+
export interface CommsThreadArtifact {
|
|
20
|
+
schema: typeof COMMS_THREAD_SCHEMA;
|
|
21
|
+
channel: CommsMessage["channel"];
|
|
22
|
+
count: number;
|
|
23
|
+
thread: CommsThreadEntry[];
|
|
24
|
+
}
|
|
25
|
+
/** Project polled inbox messages into the digest-only thread artifact. Pure; the caller writes it into
|
|
26
|
+
* the run dir and registers it as an adapter-artifact. */
|
|
27
|
+
export declare function buildCommsThreadArtifact(messages: CommsMessage[]): CommsThreadArtifact;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Digest-only evidence for a captured comms thread (#297). Proves "the verification mail arrived and
|
|
2
|
+
// the persona could act on it" WITHOUT persisting raw PHI. Written as an adapter-artifact
|
|
3
|
+
// (humanish.comms-thread.v1), so it inherits the bundle's existence-verify + public-safety scan.
|
|
4
|
+
//
|
|
5
|
+
// Digest discipline (deliberate, see below): addresses + links are digested (high entropy → the digest
|
|
6
|
+
// is not reversible). The subject is DIGESTED too, not stored as text — redactText only scrubs
|
|
7
|
+
// secret-SHAPED tokens/paths, not free-form PII (the #108 gap), so a subject like "results for <name>"
|
|
8
|
+
// would pass through verbatim; a sha256-16 keeps a PII subject non-reversible while still letting you
|
|
9
|
+
// correlate identical subjects. OTP CODES are a COUNT ONLY, never digested — a sha256 of a 6-digit code
|
|
10
|
+
// has ~10^6 preimages and is trivially brute-forced back to the code, so a "code digest" would leak it.
|
|
11
|
+
// Net: NO raw address/subject/link/OTP text ever lands in the artifact. Same caution as the lobby code.
|
|
12
|
+
import { digestText } from "./redaction.js";
|
|
13
|
+
export const COMMS_THREAD_SCHEMA = "humanish.comms-thread.v1";
|
|
14
|
+
/** Project polled inbox messages into the digest-only thread artifact. Pure; the caller writes it into
|
|
15
|
+
* the run dir and registers it as an adapter-artifact. */
|
|
16
|
+
export function buildCommsThreadArtifact(messages) {
|
|
17
|
+
const channel = messages[0]?.channel ?? "email";
|
|
18
|
+
const thread = messages.map((message) => ({
|
|
19
|
+
id: message.id,
|
|
20
|
+
channel: message.channel,
|
|
21
|
+
fromDigest: digestText(message.from, 16),
|
|
22
|
+
toDigests: message.to.map((address) => address.digest),
|
|
23
|
+
...(message.subject === undefined ? {} : { subjectDigest: digestText(message.subject, 16) }),
|
|
24
|
+
linkDigests: message.links.map((link) => digestText(link, 16)),
|
|
25
|
+
codeCount: message.codes.length,
|
|
26
|
+
sentAt: message.sentAt,
|
|
27
|
+
deliveredAt: message.deliveredAt
|
|
28
|
+
}));
|
|
29
|
+
return { schema: COMMS_THREAD_SCHEMA, channel, count: thread.length, thread };
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=comms-evidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms-evidence.js","sourceRoot":"","sources":["../src/comms-evidence.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,0FAA0F;AAC1F,iGAAiG;AACjG,EAAE;AACF,uGAAuG;AACvG,+FAA+F;AAC/F,uGAAuG;AACvG,sGAAsG;AACtG,wGAAwG;AACxG,wGAAwG;AACxG,wGAAwG;AAGxG,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,0BAA0B,CAAC;AA0B9D;2DAC2D;AAC3D,MAAM,UAAU,wBAAwB,CAAC,QAAwB;IAC/D,MAAM,OAAO,GAA4B,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,OAAO,CAAC;IACzE,MAAM,MAAM,GAAuB,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC5D,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QACxC,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;QACtD,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC;QAC5F,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC9D,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM;QAC/B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,WAAW,EAAE,OAAO,CAAC,WAAW;KACjC,CAAC,CAAC,CAAC;IACJ,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;AAChF,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CommsChannel } from "./comms-types.js";
|
|
2
|
+
import { type EmailSendProfile } from "./comms-email-catch.js";
|
|
3
|
+
import { type DetachedTimers } from "./e2b-detached.js";
|
|
4
|
+
import type { E2BDesktopSandbox } from "./e2b-desktop-launch.js";
|
|
5
|
+
/** The default in-sandbox loopback port for the catch. Fixed (not ephemeral) so the injected base-URL
|
|
6
|
+
* env is known before `createDesktopSandbox`. 8025 is the conventional local-mail-UI port and is
|
|
7
|
+
* unlikely to collide with a subject app; override via config if it does. */
|
|
8
|
+
export declare const DEFAULT_SANDBOX_CATCH_PORT = 8025;
|
|
9
|
+
/**
|
|
10
|
+
* The self-contained in-sandbox capture server (a plain node ESM string — runs on the sandbox's own
|
|
11
|
+
* node, imports nothing from humanish). It is DELIBERATELY dumb: it records each POST verbatim as an
|
|
12
|
+
* NDJSON line `{t, path, body}` and returns a plausible provider success — all normalization/profile
|
|
13
|
+
* parsing happens host-side on the drained lines, so the typed, tested profiles stay in one place.
|
|
14
|
+
* argv: <port> <deliveriesFile>.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SANDBOX_CATCH_SCRIPT: string;
|
|
17
|
+
export interface DeployCommsCatchOptions {
|
|
18
|
+
/** Fixed loopback port the catch listens on (default 8025). Must be free inside the sandbox. */
|
|
19
|
+
port?: number;
|
|
20
|
+
/** In-sandbox working dir for the script + NDJSON (default /tmp/humanish-comms). */
|
|
21
|
+
dir?: string;
|
|
22
|
+
/** Detached-process name ([a-z0-9-]); default "comms-catch". */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Readiness-probe budget (ms) for the catch's /health (default 15000). */
|
|
25
|
+
readyTimeoutMs?: number;
|
|
26
|
+
requestTimeoutMs?: number;
|
|
27
|
+
timers?: DetachedTimers;
|
|
28
|
+
}
|
|
29
|
+
export interface DeployedCommsCatch {
|
|
30
|
+
port: number;
|
|
31
|
+
/** Inject THIS as the app's email-API base URL (e.g. RESEND_API_URL) — the sandbox's own loopback. */
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
deliveriesPath: string;
|
|
34
|
+
/** Whether the catch's /health returned OUR service marker within the readiness budget. Callers MUST
|
|
35
|
+
* treat `ready === false` as fatal (do not inject baseUrl into a dead catch — the app's sends would
|
|
36
|
+
* silently fail with nothing captured). */
|
|
37
|
+
ready: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** A raw send the in-sandbox catch captured (host-side parsing happens in routeCapturedSends). */
|
|
40
|
+
export interface RawCapturedSend {
|
|
41
|
+
path: string;
|
|
42
|
+
body: string;
|
|
43
|
+
t: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write + launch the in-sandbox catch (detached), then probe it ready. Call AFTER the subject sandbox
|
|
47
|
+
* is created and BEFORE the subject app's serve.start, so the base URL resolves at the app's boot.
|
|
48
|
+
*/
|
|
49
|
+
export declare function deployCommsCatch(desktop: E2BDesktopSandbox, options?: DeployCommsCatchOptions): Promise<DeployedCommsCatch>;
|
|
50
|
+
/**
|
|
51
|
+
* Drain new captured sends from the in-sandbox NDJSON since `cursor` (a line count). Returns the fresh
|
|
52
|
+
* sends and the new cursor. Cheap `cat` over commands.run; NDJSON is small for a run.
|
|
53
|
+
*/
|
|
54
|
+
export declare function drainCommsCatch(desktop: E2BDesktopSandbox, deployed: Pick<DeployedCommsCatch, "deliveriesPath">, cursor?: number, requestTimeoutMs?: number): Promise<{
|
|
55
|
+
sends: RawCapturedSend[];
|
|
56
|
+
cursor: number;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* Parse drained raw sends with the host profiles and route them into the CommsChannel (the host-side
|
|
60
|
+
* FauxInbox). Returns the number of inbox deliveries made. Same profiles as the host catch, so the
|
|
61
|
+
* in-sandbox and in-process routes normalize identically.
|
|
62
|
+
*/
|
|
63
|
+
export declare function routeCapturedSends(sends: RawCapturedSend[], channel: CommsChannel, profiles?: EmailSendProfile[]): Promise<number>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Deploy the vendor-neutral email catch INSIDE the subject E2B sandbox and bridge captured sends back
|
|
2
|
+
// to the host bus (#297 config-block core). The host `startEmailCatchServer` binds the HOST's loopback,
|
|
3
|
+
// which a sandboxed app cannot reach — `127.0.0.1:PORT` from the app is the SANDBOX's loopback. So the
|
|
4
|
+
// listener must live in the sandbox: we write a tiny self-contained capture server (no deps, no host
|
|
5
|
+
// import) into the sandbox, launch it detached (the same substrate that serves the subject app), and
|
|
6
|
+
// each poll `cat` its append-only NDJSON of captured sends back to the host, where the real profiles
|
|
7
|
+
// parse them and route into the CommsChannel. A FIXED loopback port is chosen up front so the app's
|
|
8
|
+
// injected base-URL env (`http://127.0.0.1:<port>`) is known before the sandbox is created.
|
|
9
|
+
import { DEFAULT_EMAIL_PROFILES } from "./comms-email-catch.js";
|
|
10
|
+
import { startDetachedProcess } from "./e2b-detached.js";
|
|
11
|
+
/** The default in-sandbox loopback port for the catch. Fixed (not ephemeral) so the injected base-URL
|
|
12
|
+
* env is known before `createDesktopSandbox`. 8025 is the conventional local-mail-UI port and is
|
|
13
|
+
* unlikely to collide with a subject app; override via config if it does. */
|
|
14
|
+
export const DEFAULT_SANDBOX_CATCH_PORT = 8025;
|
|
15
|
+
const DEFAULT_CATCH_DIR = "/tmp/humanish-comms";
|
|
16
|
+
/** Single-quote for safe shell interpolation. */
|
|
17
|
+
function shq(value) {
|
|
18
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The self-contained in-sandbox capture server (a plain node ESM string — runs on the sandbox's own
|
|
22
|
+
* node, imports nothing from humanish). It is DELIBERATELY dumb: it records each POST verbatim as an
|
|
23
|
+
* NDJSON line `{t, path, body}` and returns a plausible provider success — all normalization/profile
|
|
24
|
+
* parsing happens host-side on the drained lines, so the typed, tested profiles stay in one place.
|
|
25
|
+
* argv: <port> <deliveriesFile>.
|
|
26
|
+
*/
|
|
27
|
+
export const SANDBOX_CATCH_SCRIPT = [
|
|
28
|
+
'import { createServer } from "node:http";',
|
|
29
|
+
'import { appendFileSync, mkdirSync } from "node:fs";',
|
|
30
|
+
'import { dirname } from "node:path";',
|
|
31
|
+
'const port = Number(process.argv[2] || 8025);',
|
|
32
|
+
'const outFile = process.argv[3] || "/tmp/humanish-comms/deliveries.ndjson";',
|
|
33
|
+
'try { mkdirSync(dirname(outFile), { recursive: true }); } catch {}',
|
|
34
|
+
'const MAX = 5 * 1024 * 1024;',
|
|
35
|
+
'createServer((req, res) => {',
|
|
36
|
+
' const path = (req.url || "/").split("?")[0];',
|
|
37
|
+
' 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; }',
|
|
38
|
+
' if (req.method !== "POST") { res.writeHead(404, { "content-type": "application/json" }); res.end(JSON.stringify({ error: "not found" })); return; }',
|
|
39
|
+
' let size = 0; const chunks = [];',
|
|
40
|
+
' req.on("data", (c) => { size += c.length; if (size > MAX) { req.destroy(); } else { chunks.push(c); } });',
|
|
41
|
+
' req.on("end", () => {',
|
|
42
|
+
' const body = Buffer.concat(chunks).toString("utf8");',
|
|
43
|
+
' try { appendFileSync(outFile, JSON.stringify({ t: Date.now(), path, body }) + "\\n"); } catch {}',
|
|
44
|
+
' const id = "humanish-catch-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);',
|
|
45
|
+
' if (path === "/v3/mail/send") { res.writeHead(202, { "x-message-id": id }); res.end(); }',
|
|
46
|
+
' else if (path.endsWith("/batch")) { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ data: [{ id }] })); }',
|
|
47
|
+
' else { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ id })); }',
|
|
48
|
+
' });',
|
|
49
|
+
' req.on("error", () => { try { res.writeHead(400); res.end(); } catch {} });',
|
|
50
|
+
'}).listen(port, "127.0.0.1");'
|
|
51
|
+
].join("\n");
|
|
52
|
+
/** Readiness probe that asserts OUR service marker in the /health body (not merely any 2xx) — so a
|
|
53
|
+
* process squatting on the fixed port cannot produce a false "ready" while the app's sends bypass us. */
|
|
54
|
+
async function catchHealthy(desktop, port, options) {
|
|
55
|
+
const now = options.now ?? Date.now;
|
|
56
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
57
|
+
const deadline = now() + options.timeoutMs;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const result = await desktop.commands
|
|
60
|
+
.run(`curl -s --max-time 5 http://127.0.0.1:${port}/health 2>/dev/null || true`, { requestTimeoutMs: options.requestTimeoutMs })
|
|
61
|
+
.catch(() => ({ stdout: "" }));
|
|
62
|
+
if ((result.stdout ?? "").includes("humanish-comms-catch"))
|
|
63
|
+
return true;
|
|
64
|
+
if (now() >= deadline)
|
|
65
|
+
return false;
|
|
66
|
+
await sleep(1000);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Write + launch the in-sandbox catch (detached), then probe it ready. Call AFTER the subject sandbox
|
|
71
|
+
* is created and BEFORE the subject app's serve.start, so the base URL resolves at the app's boot.
|
|
72
|
+
*/
|
|
73
|
+
export async function deployCommsCatch(desktop, options = {}) {
|
|
74
|
+
// Validate the port to an integer before it reaches the shell command (defense-in-depth: a future
|
|
75
|
+
// caller might cast a config value; the value is typed `number` but this makes injection impossible).
|
|
76
|
+
const port = Math.trunc(Number(options.port ?? DEFAULT_SANDBOX_CATCH_PORT));
|
|
77
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
|
|
78
|
+
throw new Error(`deployCommsCatch: invalid port ${JSON.stringify(options.port)}`);
|
|
79
|
+
}
|
|
80
|
+
const dir = options.dir ?? DEFAULT_CATCH_DIR;
|
|
81
|
+
const name = options.name ?? "comms-catch";
|
|
82
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
83
|
+
const scriptPath = `${dir}/catch.mjs`;
|
|
84
|
+
const deliveriesPath = `${dir}/deliveries.ndjson`;
|
|
85
|
+
await desktop.commands.run(`mkdir -p ${shq(dir)}`, { requestTimeoutMs });
|
|
86
|
+
await desktop.files.write(scriptPath, SANDBOX_CATCH_SCRIPT);
|
|
87
|
+
await startDetachedProcess(desktop, {
|
|
88
|
+
name,
|
|
89
|
+
command: `node ${shq(scriptPath)} ${port} ${shq(deliveriesPath)}`,
|
|
90
|
+
requestTimeoutMs
|
|
91
|
+
});
|
|
92
|
+
const ready = await catchHealthy(desktop, port, {
|
|
93
|
+
timeoutMs: options.readyTimeoutMs ?? 15_000,
|
|
94
|
+
requestTimeoutMs,
|
|
95
|
+
...(options.timers ?? {})
|
|
96
|
+
});
|
|
97
|
+
return { port, baseUrl: `http://127.0.0.1:${port}`, deliveriesPath, ready };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Drain new captured sends from the in-sandbox NDJSON since `cursor` (a line count). Returns the fresh
|
|
101
|
+
* sends and the new cursor. Cheap `cat` over commands.run; NDJSON is small for a run.
|
|
102
|
+
*/
|
|
103
|
+
export async function drainCommsCatch(desktop, deployed, cursor = 0, requestTimeoutMs = 30_000) {
|
|
104
|
+
const result = await desktop.commands.run(`cat ${shq(deployed.deliveriesPath)} 2>/dev/null || true`, { requestTimeoutMs });
|
|
105
|
+
const stdout = result.stdout ?? "";
|
|
106
|
+
let lines = stdout.split("\n").filter((line) => line.trim().length > 0);
|
|
107
|
+
// If the file doesn't end in a newline, the last line may be a PARTIAL append (the host `cat` raced
|
|
108
|
+
// an in-sandbox append of a large body). Drop it and don't advance the cursor past it — it re-reads
|
|
109
|
+
// complete on the next poll, so a captured send is never lost to the race (the script only ever emits
|
|
110
|
+
// valid JSON, so an incomplete line is the only cause of a parse miss).
|
|
111
|
+
if (!stdout.endsWith("\n") && lines.length > 0)
|
|
112
|
+
lines = lines.slice(0, -1);
|
|
113
|
+
const sends = [];
|
|
114
|
+
for (const line of lines.slice(cursor)) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(line);
|
|
117
|
+
if (typeof parsed.path === "string" && typeof parsed.body === "string") {
|
|
118
|
+
sends.push({ path: parsed.path, body: parsed.body, t: typeof parsed.t === "number" ? parsed.t : 0 });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// skip a malformed line
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { sends, cursor: lines.length };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Parse drained raw sends with the host profiles and route them into the CommsChannel (the host-side
|
|
129
|
+
* FauxInbox). Returns the number of inbox deliveries made. Same profiles as the host catch, so the
|
|
130
|
+
* in-sandbox and in-process routes normalize identically.
|
|
131
|
+
*/
|
|
132
|
+
export async function routeCapturedSends(sends, channel, profiles = DEFAULT_EMAIL_PROFILES) {
|
|
133
|
+
let delivered = 0;
|
|
134
|
+
for (const send of sends) {
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = JSON.parse(send.body.length > 0 ? send.body : "{}");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const profile = profiles.find((candidate) => candidate.sendPaths.includes(send.path)) ?? profiles[0];
|
|
143
|
+
if (profile === undefined)
|
|
144
|
+
continue;
|
|
145
|
+
for (const normalized of profile.parse(send.path, parsed)) {
|
|
146
|
+
const messages = await channel.deliverRaw({
|
|
147
|
+
from: normalized.from,
|
|
148
|
+
to: normalized.to,
|
|
149
|
+
...(normalized.subject === undefined ? {} : { subject: normalized.subject }),
|
|
150
|
+
body: normalized.body
|
|
151
|
+
});
|
|
152
|
+
delivered += messages.length;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return delivered;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=comms-sandbox-catch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms-sandbox-catch.js","sourceRoot":"","sources":["../src/comms-sandbox-catch.ts"],"names":[],"mappings":"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"}
|
package/dist/cua-actor-lab.js
CHANGED
|
@@ -31,6 +31,7 @@ import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS, chromiumEvidenceProfilePreferencesJson
|
|
|
31
31
|
import { DEFAULT_OPENAI_CU_MODEL } from "./openai-responses-cu.js";
|
|
32
32
|
import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
33
33
|
import { probeUrl, readDetachedLog, runDetachedStep, startDetachedProcess } from "./e2b-detached.js";
|
|
34
|
+
import { DEFAULT_SANDBOX_CATCH_PORT, deployCommsCatch } from "./comms-sandbox-catch.js";
|
|
34
35
|
import { DEFAULT_DEVICE_PRESET, isDevicePresetName, resolveDevicePreset } from "./device-presets.js";
|
|
35
36
|
import { cuaLaneValidationReason, isHttpUrl, isLoopbackUrl, MAX_CUA_LANES, subjectStateInvalidReason } from "./lab-config.js";
|
|
36
37
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
@@ -987,6 +988,15 @@ export async function runCuaLane(spec, deps) {
|
|
|
987
988
|
const { config, appUrl, cloneRoute, localTreeRoute, serve, subjectRepo, subjectEnvNames } = deps;
|
|
988
989
|
const targetUrl = spec.targetUrl ?? appUrl;
|
|
989
990
|
const env = deps.env;
|
|
991
|
+
// Off-app comms (#297): on an in-sandbox subject route, redirect the app's email-API sends into an
|
|
992
|
+
// in-sandbox catch (loopback) so its verification mail is CAPTURED, not sent to the internet. Gated
|
|
993
|
+
// ENTIRELY on config.comms — no comms declared → zero change. The base-URL env is injected at
|
|
994
|
+
// sandbox-create (below, so the app reads it at boot); the catch is started right after create.
|
|
995
|
+
const commsEmail = (cloneRoute || localTreeRoute) ? config.comms?.email : undefined;
|
|
996
|
+
const commsPort = commsEmail ? (commsEmail.port ?? DEFAULT_SANDBOX_CATCH_PORT) : undefined;
|
|
997
|
+
const commsEnv = commsEmail && commsPort !== undefined
|
|
998
|
+
? { [commsEmail.injectEnv]: `http://127.0.0.1:${commsPort}` }
|
|
999
|
+
: {};
|
|
990
1000
|
const warnings = [];
|
|
991
1001
|
const screenshots = [];
|
|
992
1002
|
const writeScreenshot = makeLaneWriteScreenshot(deps.artifactRoot, spec, screenshots);
|
|
@@ -1055,8 +1065,8 @@ export async function runCuaLane(spec, deps) {
|
|
|
1055
1065
|
},
|
|
1056
1066
|
// Env placement per the doctrine: the ACTOR's key never enters the sandbox (the model drives
|
|
1057
1067
|
// from outside). The SUBJECT's declared env NAMES are provisioned here on the clone route.
|
|
1058
|
-
...(subjectEnvNames.length > 0
|
|
1059
|
-
? { envs: Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])) }
|
|
1068
|
+
...(subjectEnvNames.length > 0 || Object.keys(commsEnv).length > 0
|
|
1069
|
+
? { envs: { ...Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])), ...commsEnv } }
|
|
1060
1070
|
: {}),
|
|
1061
1071
|
resolution: spec.resolution,
|
|
1062
1072
|
dpi: 96,
|
|
@@ -1068,6 +1078,15 @@ export async function runCuaLane(spec, deps) {
|
|
|
1068
1078
|
if (deps.hooks.prepareDesktop) {
|
|
1069
1079
|
await deps.hooks.prepareDesktop(desktop, { laneId: spec.laneId, laneIndex: spec.laneIndex, laneCount: deps.laneCount });
|
|
1070
1080
|
}
|
|
1081
|
+
// Start the in-sandbox email catch BEFORE the subject serve, so the app's send-API base URL (injected
|
|
1082
|
+
// into its env at create) resolves the moment it boots. A comms-declared lab that can't stand the
|
|
1083
|
+
// catch up is a setup failure (fail closed) rather than silently sending real mail.
|
|
1084
|
+
if (commsEmail && commsPort !== undefined) {
|
|
1085
|
+
const deployedComms = await deployCommsCatch(desktop, { port: commsPort, requestTimeoutMs: deps.requestTimeoutMs });
|
|
1086
|
+
if (!deployedComms.ready) {
|
|
1087
|
+
throw new Error(`comms email catch did not become ready on 127.0.0.1:${commsPort} in the subject sandbox`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1071
1090
|
// Per-lane geometry assertion (fail-closed) — the device claim is verified in-sandbox.
|
|
1072
1091
|
const screenGeometry = await inspectDesktopScreenGeometry({
|
|
1073
1092
|
desktop,
|