humanish 0.26.0 → 0.28.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.
@@ -31,8 +31,9 @@ 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, collectCommsThread, deployCommsCatch } from "./comms-sandbox-catch.js";
34
+ import { DEFAULT_SANDBOX_CATCH_PORT, collectCommsThread, deployCommsCatch, refreshInboxSurface, writeInboxSurface } from "./comms-sandbox-catch.js";
35
35
  import { FakeInbox } from "./comms-fake-inbox.js";
36
+ import { buildOriginMap } from "./comms-inbox.js";
36
37
  import { DEFAULT_DEVICE_PRESET, isDevicePresetName, resolveDevicePreset } from "./device-presets.js";
37
38
  import { cuaLaneValidationReason, isHttpUrl, isLoopbackUrl, MAX_CUA_LANES, subjectStateInvalidReason } from "./lab-config.js";
38
39
  import { mapWithConcurrency } from "./concurrency.js";
@@ -110,6 +111,25 @@ export function composeLaneInstructions(args) {
110
111
  }
111
112
  };
112
113
  }
114
+ /** Runtime-inject the persona inbox instruction into a lane's prompt (#297 slice B). The inbox URL is a
115
+ * runtime loopback/getHost address (not secret), so — mirroring the lobby-code runtime injection — this
116
+ * augments only the instructions the model receives; the authored prompt + its digest are unchanged.
117
+ * Returns a new spec (never mutates). Shared by the CUA + concurrent shared-world routes. */
118
+ export function withInboxMission(spec, inboxUrl) {
119
+ return {
120
+ ...spec,
121
+ instructions: `${spec.instructions}\n\nEmail inbox: when the app tells you it has emailed you (a verification link, confirmation code, or magic link), open ${inboxUrl} in the browser to read that email and follow its link or enter its code. All email the app sends you arrives there.`
122
+ };
123
+ }
124
+ /** True when a lane has a declared comms recipient WITH an address, so the drain can actually match the
125
+ * mail the persona will be told to read. Gates the inbox instruction to lanes that can receive mail —
126
+ * a lane told to check an inbox it can never receive into would just stall. */
127
+ export function laneHasInboxRecipient(commsEmail, laneId) {
128
+ return (commsEmail.recipients ?? []).some((recipient) => recipient.lane === laneId && recipient.address !== undefined);
129
+ }
130
+ /** Mid-run inbox-surface render cadence (ms). Coarse enough that the per-tick `cat` + file writes stay
131
+ * cheap; fine enough that a verification email is visible seconds after the app sends it. */
132
+ const INBOX_SURFACE_CADENCE_MS = 2500;
113
133
  /**
114
134
  * The narrowest browser WINDOW Chrome/Chromium will render on the E2B desktop. Chrome refuses to
115
135
  * make its window narrower than this (~500 CSS px observed: a 414-wide X screen produced a 500-wide
@@ -1002,6 +1022,26 @@ export async function runCuaLane(spec, deps) {
1002
1022
  const commsEnv = commsEmail && commsPort !== undefined
1003
1023
  ? { [commsEmail.injectEnv]: `http://127.0.0.1:${commsPort}` }
1004
1024
  : {};
1025
+ // Persona inbox SURFACE (#297 slice B): the loopback URL the persona opens to read captured mail; the
1026
+ // origin-rewrite map (identity on this same-sandbox route, but covers localhost/0.0.0.0 alias skew + an
1027
+ // operator-declared linkOrigin); and a disposable background loop that renders the surface DURING the
1028
+ // session so the inbox is live when the persona checks. The surface uses its OWN FakeInbox + cursor,
1029
+ // independent of the teardown evidence drain (two readers of the append-only NDJSON — no double-count).
1030
+ const commsInboxUrl = commsEmail && commsPort !== undefined ? `http://127.0.0.1:${commsPort}/inbox` : undefined;
1031
+ const commsOriginMap = commsEmail
1032
+ ? buildOriginMap({
1033
+ ...(config.subject.serve?.url === undefined ? {} : { internalServeUrl: config.subject.serve.url }),
1034
+ reachableBaseUrl: targetUrl,
1035
+ ...(commsEmail.linkOrigin === undefined ? {} : { linkOrigin: commsEmail.linkOrigin })
1036
+ })
1037
+ : [];
1038
+ let surfaceChannel;
1039
+ const surfaceInboxes = [];
1040
+ let surfaceCursor = 0;
1041
+ let surfaceDisposed = false;
1042
+ let releaseSurface = () => { };
1043
+ const surfaceDispose = new Promise((resolve) => { releaseSurface = resolve; });
1044
+ let surfaceLoop;
1005
1045
  const warnings = [];
1006
1046
  const screenshots = [];
1007
1047
  const writeScreenshot = makeLaneWriteScreenshot(deps.artifactRoot, spec, screenshots);
@@ -1091,6 +1131,51 @@ export async function runCuaLane(spec, deps) {
1091
1131
  if (!deployedComms.ready) {
1092
1132
  throw new Error(`comms email catch did not become ready on 127.0.0.1:${commsPort} in the subject sandbox`);
1093
1133
  }
1134
+ // Stand up the live inbox surface: provision the declared-recipient inboxes on a dedicated surface
1135
+ // channel, then start a background loop that drains-and-renders on a cadence so the persona sees new
1136
+ // mail mid-session. Disposed at the TOP of the finally, before the teardown evidence drain.
1137
+ surfaceChannel = new FakeInbox();
1138
+ for (const recipient of commsEmail.recipients ?? []) {
1139
+ if (recipient.address !== undefined)
1140
+ surfaceInboxes.push(await surfaceChannel.provisionAddress(recipient.lane, recipient.address));
1141
+ }
1142
+ // Write the EMPTY inbox once up front so the persona's /inbox always resolves to the "No messages
1143
+ // yet." page — never a bare 404 — the instant it navigates there, even before any mail arrives OR if
1144
+ // the app sends to an address no declared recipient matches (the loop only re-renders on new mail).
1145
+ await writeInboxSurface(desktop, deployedComms.surfaceDir, [], { originMap: commsOriginMap, requestTimeoutMs: deps.requestTimeoutMs });
1146
+ const deployedRef = deployedComms;
1147
+ const surfaceChannelRef = surfaceChannel;
1148
+ surfaceLoop = (async () => {
1149
+ // Render-first (so even a short session gets a populated inbox), then refresh on a cadence. The
1150
+ // cadence uses a REAL timer, NOT the injected instant clock: this loop is unbounded, so an instant
1151
+ // sleep would busy-spin and starve the session's own timers. The wait is interruptible by
1152
+ // surfaceDispose (and the timer cleared) so teardown never blocks for a full cadence.
1153
+ for (;;) {
1154
+ try {
1155
+ const refreshed = await refreshInboxSurface({
1156
+ desktop,
1157
+ deployed: deployedRef,
1158
+ channel: surfaceChannelRef,
1159
+ inboxes: surfaceInboxes,
1160
+ cursor: surfaceCursor,
1161
+ originMap: commsOriginMap,
1162
+ requestTimeoutMs: deps.requestTimeoutMs
1163
+ });
1164
+ surfaceCursor = refreshed.cursor;
1165
+ }
1166
+ catch {
1167
+ // Never throw into the render loop; the teardown drain + by-id teardown must still run.
1168
+ }
1169
+ if (surfaceDisposed)
1170
+ break;
1171
+ await new Promise((resolve) => {
1172
+ const timer = setTimeout(resolve, INBOX_SURFACE_CADENCE_MS);
1173
+ void surfaceDispose.then(() => { clearTimeout(timer); resolve(); });
1174
+ });
1175
+ if (surfaceDisposed)
1176
+ break;
1177
+ }
1178
+ })();
1094
1179
  }
1095
1180
  // Per-lane geometry assertion (fail-closed) — the device claim is verified in-sandbox.
1096
1181
  const screenGeometry = await inspectDesktopScreenGeometry({
@@ -1210,7 +1295,11 @@ export async function runCuaLane(spec, deps) {
1210
1295
  const capModelId = config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL;
1211
1296
  const maxUsd = config.execution?.caps?.maxUsd;
1212
1297
  const sessionOptions = {
1213
- instructions: spec.instructions,
1298
+ // Tell the persona where its inbox is — but only when comms is live AND this lane has a declared
1299
+ // recipient it can actually receive mail into (else it would stall on an inbox that stays empty).
1300
+ instructions: commsEmail && commsInboxUrl && deployedComms?.ready && laneHasInboxRecipient(commsEmail, spec.laneId)
1301
+ ? withInboxMission(spec, commsInboxUrl).instructions
1302
+ : spec.instructions,
1214
1303
  persona: spec.persona,
1215
1304
  timeoutMs: deps.timeoutMs,
1216
1305
  openai: {
@@ -1252,6 +1341,13 @@ export async function runCuaLane(spec, deps) {
1252
1341
  sessionError = redactText(deps.scrubKnownValues(toErrorMessage(error)));
1253
1342
  }
1254
1343
  finally {
1344
+ // Stop the mid-run inbox-surface loop FIRST — before the teardown evidence drain below — so the two
1345
+ // `cat`s never overlap and the final surface state is deterministic. A surface failure can never
1346
+ // block teardown (the loop body is fully try/caught and this await is on its already-caught promise).
1347
+ surfaceDisposed = true;
1348
+ releaseSurface();
1349
+ if (surfaceLoop)
1350
+ await surfaceLoop.catch(() => undefined);
1255
1351
  if (!provisioned) {
1256
1352
  signal(false);
1257
1353
  }