humanish 0.22.0 → 0.24.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.
@@ -1,5 +1,5 @@
1
1
  // A VENDOR-NEUTRAL loopback catch server for email-send APIs (#297). An app hardwired to a hosted
2
- // email provider is redirected into the faux bus with ONE env var and no code change: point the app's
2
+ // email provider is redirected into the fake bus with ONE env var and no code change: point the app's
3
3
  // API base URL at this server. The catch does not depend on, or name itself after, any one vendor —
4
4
  // it normalizes each provider's distinct wire shape (Resend's flat body, SendGrid's nested
5
5
  // personalizations, Postmark's TitleCase, a custom app's own JSON) to one shape via pluggable
@@ -6,7 +6,7 @@ export declare function extractLinks(body: string): string[];
6
6
  * "verification code 8A3F2K") are high-precision and preferred; if none are labeled, fall back to an
7
7
  * isolated 4–8 digit run (a bare OTP). Pure; tokens are runtime-only literal-scrub targets. */
8
8
  export declare function extractOtpCodes(body: string): string[];
9
- export interface FauxInboxOptions {
9
+ export interface FakeInboxOptions {
10
10
  /** "email" (default) or "sms" — the address shape + surface differ; machinery is identical. */
11
11
  channel?: CommsChannelKind;
12
12
  /** Email domain for minted addresses. Default example.test (an RFC 6761 reserved, unroutable test
@@ -15,17 +15,17 @@ export interface FauxInboxOptions {
15
15
  /** Injected clock (ms) for deterministic tests. Default Date.now. */
16
16
  now?: () => number;
17
17
  }
18
- /** The in-process faux adapter. Implements the same CommsChannel port a real provider adapter would. */
19
- export declare class FauxInbox implements CommsChannel {
18
+ /** The in-process fake adapter. Implements the same CommsChannel port a real provider adapter would. */
19
+ export declare class FakeInbox implements CommsChannel {
20
20
  readonly channel: CommsChannelKind;
21
- readonly kind: "faux";
21
+ readonly kind: "fake";
22
22
  private readonly domain;
23
23
  private readonly clock;
24
24
  private readonly byActor;
25
25
  private readonly byValue;
26
26
  private readonly queues;
27
27
  private counter;
28
- constructor(options?: FauxInboxOptions);
28
+ constructor(options?: FakeInboxOptions);
29
29
  provision(actorId: string): Promise<CommsAddress>;
30
30
  private route;
31
31
  send(message: OutboundMessage): Promise<CommsMessage>;
@@ -1,4 +1,7 @@
1
- // The FAUX in-process email/SMS bus (#297 Stage 2). Deterministic, $0, offline, public-safe: a
1
+ // The FAKE in-process email/SMS bus (#297 Stage 2). "Fake" in the precise test-double sense (Fowler):
2
+ // a working, in-memory implementation of the CommsChannel port that takes a production shortcut — the
3
+ // same slot as Kubernetes' `fake` clientset, distinct from a mock/stub. Deterministic, $0, offline,
4
+ // public-safe: a
2
5
  // message an app-under-test "sends" (via an ingress like the vendor-neutral email catch) is routed to the
3
6
  // addressed actor inbox and read back through the same CommsChannel port a real provider adapter
4
7
  // would implement. Nothing leaves the process. See comms-types.ts for the port + public-safety notes.
@@ -73,10 +76,10 @@ function smsAddressFor(actorId) {
73
76
  const digits = digestText(actorId, 16).replace(/[a-f]/g, (c) => String(c.charCodeAt(0) % 10)).slice(0, 7);
74
77
  return `+1555${digits}`;
75
78
  }
76
- /** The in-process faux adapter. Implements the same CommsChannel port a real provider adapter would. */
77
- export class FauxInbox {
79
+ /** The in-process fake adapter. Implements the same CommsChannel port a real provider adapter would. */
80
+ export class FakeInbox {
78
81
  channel;
79
- kind = "faux";
82
+ kind = "fake";
80
83
  domain;
81
84
  clock;
82
85
  byActor = new Map();
@@ -95,7 +98,7 @@ export class FauxInbox {
95
98
  const value = this.channel === "sms" ? smsAddressFor(actorId) : `${sanitizeLocalPart(actorId)}@${this.domain}`;
96
99
  // Address collision guard: two distinct actor ids can sanitize to the same local part. Reuse the
97
100
  // existing inbox rather than resetting its queue (which would drop already-delivered mail). Both
98
- // actors then share it — a faux-world edge; declare distinct addresses to avoid it.
101
+ // actors then share it — a fake-world edge; declare distinct addresses to avoid it.
99
102
  const prior = this.byValue.get(value.toLowerCase());
100
103
  if (prior) {
101
104
  this.byActor.set(actorId, prior);
@@ -154,4 +157,4 @@ export class FauxInbox {
154
157
  return [...this.byActor.values()];
155
158
  }
156
159
  }
157
- //# sourceMappingURL=comms-faux-inbox.js.map
160
+ //# sourceMappingURL=comms-fake-inbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comms-fake-inbox.js","sourceRoot":"","sources":["../src/comms-fake-inbox.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,sGAAsG;AACtG,oGAAoG;AACpG,iBAAiB;AACjB,0GAA0G;AAC1G,iGAAiG;AACjG,sGAAsG;AAEtG,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAU5C;qFACqF;AACrF,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7D,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,IAAI,GAAG,CAAC,GAAW,EAAQ,EAAE;QACjC,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QACjD,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAChD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChB,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAyB,CAAC;IAC9B,MAAM,IAAI,GAAG,+BAA+B,CAAC;IAC7C,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI;QAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,4BAA4B,CAAC;IAC1C,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI;QAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI;SACR,OAAO,CAAC,2BAA2B,EAAE,GAAG,CAAC;SACzC,OAAO,CAAC,6BAA6B,EAAE,GAAG,CAAC;SAC3C,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;gGAEgG;AAChG,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7D,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,IAAI,GAAG,CAAC,IAAc,EAAE,IAAY,EAAQ,EAAE;QAClD,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACZ,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IACF,oGAAoG;IACpG,0FAA0F;IAC1F,MAAM,SAAS,GAAG,qNAAqN,CAAC;IACxO,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI;QAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACtE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACpD,+FAA+F;IAC/F,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,8CAA8C,CAAC;IAC9D,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI;QAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAe;IACxC,OAAO,OAAO,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC;AACjG,CAAC;AAED,SAAS,aAAa,CAAC,OAAe;IACpC,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1G,OAAO,QAAQ,MAAM,EAAE,CAAC;AAC1B,CAAC;AAYD,wGAAwG;AACxG,MAAM,OAAO,SAAS;IACX,OAAO,CAAmB;IAC1B,IAAI,GAAG,MAAe,CAAC;IACf,MAAM,CAAS;IACf,KAAK,CAAe;IACpB,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC1C,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC1C,MAAM,GAAG,IAAI,GAAG,EAA0B,CAAC;IACpD,OAAO,GAAG,CAAC,CAAC;IAEpB,YAAY,UAA4B,EAAE;QACxC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC;QAC1C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAC;QAC/C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAW,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,OAAe;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAC/G,iGAAiG;QACjG,iGAAiG;QACjG,oFAAoF;QACpF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QACpD,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACjC,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,OAAO,GAAiB,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC;QACvG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;QACzC,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,KAAK,CAAC,IAAY,EAAE,EAAkB,EAAE,OAA2B,EAAE,IAAY;QACvF,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QACxB,MAAM,OAAO,GAAiB;YAC5B,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;YAC9D,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI;YACJ,EAAE;YACF,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;YAC7C,IAAI;YACJ,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC;YACzB,KAAK,EAAE,eAAe,CAAC,IAAI,CAAC;YAC5B,MAAM,EAAE,EAAE;YACV,WAAW,EAAE,EAAE;SAChB,CAAC;QACF,KAAK,MAAM,IAAI,IAAI,EAAE,EAAE,CAAC;YACtB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YACxD,IAAI,KAAK;gBAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAwB;QACjC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,OAAmB;QAClC,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC;aAC1B,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;aAChE,MAAM,CAAC,CAAC,OAAO,EAA2B,EAAE,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;QACvE,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC,CAAC,uDAAuD;QACvF,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAqB,EAAE,KAAK,GAAG,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QACjE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;IACnB,CAAC;IAED,gFAAgF;IAChF,SAAS;QACP,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpC,CAAC;CACF"}
@@ -57,7 +57,7 @@ export declare function drainCommsCatch(desktop: E2BDesktopSandbox, deployed: Pi
57
57
  }>;
58
58
  /**
59
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
60
+ * FakeInbox). Returns the number of inbox deliveries made. Same profiles as the host catch, so the
61
61
  * in-sandbox and in-process routes normalize identically.
62
62
  */
63
63
  export declare function routeCapturedSends(sends: RawCapturedSend[], channel: CommsChannel, profiles?: EmailSendProfile[]): Promise<number>;
@@ -126,7 +126,7 @@ export async function drainCommsCatch(desktop, deployed, cursor = 0, requestTime
126
126
  }
127
127
  /**
128
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
129
+ * FakeInbox). Returns the number of inbox deliveries made. Same profiles as the host catch, so the
130
130
  * in-sandbox and in-process routes normalize identically.
131
131
  */
132
132
  export async function routeCapturedSends(sends, channel, profiles = DEFAULT_EMAIL_PROFILES) {
@@ -18,7 +18,7 @@ export interface CommsMessage {
18
18
  /** Resolved recipient inboxes this message was delivered to. */
19
19
  to: CommsAddress[];
20
20
  subject?: string;
21
- /** Runtime-only for real; local-only for faux (never a share path without redaction — #108). */
21
+ /** Runtime-only for real; local-only for fake (never a share path without redaction — #108). */
22
22
  body: string;
23
23
  /** Actionable links extracted from the body (magic-link / invite / reset). Runtime-only. */
24
24
  links: string[];
@@ -43,12 +43,12 @@ export interface InboundRaw {
43
43
  body: string;
44
44
  }
45
45
  /**
46
- * The port. Faux (in-process) + real-email + real-sms adapters implement it identically. Async so a
47
- * real (network) adapter fits without changing callers; the faux adapter just resolves immediately.
46
+ * The port. Fake (in-process) + real-email + real-sms adapters implement it identically. Async so a
47
+ * real (network) adapter fits without changing callers; the fake adapter just resolves immediately.
48
48
  */
49
49
  export interface CommsChannel {
50
50
  readonly channel: CommsChannelKind;
51
- readonly kind: "faux" | "real";
51
+ readonly kind: "fake" | "real";
52
52
  /** Mint/allocate an inbox for an actor (address auto-generated). Idempotent per actor. */
53
53
  provision(actorId: string): Promise<CommsAddress>;
54
54
  /** Route a composed message from one actor to addressed inboxes. Returns the delivered record. */
@@ -1,6 +1,6 @@
1
1
  // The addressed message bus (#297) — the seam that makes "off-app" comms (email/SMS the persona
2
2
  // actually lives in) a first-class, persona-driven testable surface. A single port, addressed by
3
- // actor; faux (in-process) and real (provider-backed) adapters implement it identically, so the
3
+ // actor; fake (in-process) and real (provider-backed) adapters implement it identically, so the
4
4
  // persona surface + evidence writer consume it without knowing which is behind it.
5
5
  //
6
6
  // PUBLIC-SAFETY: raw address values, message bodies, links, and codes are RUNTIME-ONLY. Only the
@@ -919,7 +919,7 @@ export async function runConcurrentSharedWorld(options) {
919
919
  //
920
920
  // TEMPORARY SHIM (tracked by #296): this CDP URL-relay handoff — reading the host's /lobby/CODE off
921
921
  // its own browser and threading it into the follower missions — is a temporary coordination shim.
922
- // It is to be augmented/replaced by the actor message bus (faux SMS/email invite) in #297: the
922
+ // It is to be augmented/replaced by the actor message bus (fake SMS/email invite) in #297: the
923
923
  // human-realistic version is the HOST SENDING the invite link and followers RECEIVING and tapping
924
924
  // it, rather than the orchestrator relaying the code out-of-band.
925
925
  const lobbyCodeLatch = deferred();
@@ -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,