humanish 0.95.0 → 0.96.1
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 +4 -2
- package/dist/comms-agentmail.d.ts +25 -0
- package/dist/comms-agentmail.js +418 -0
- package/dist/comms-agentmail.js.map +1 -0
- package/dist/comms-connections.d.ts +4 -2
- package/dist/comms-connections.js +3 -3
- package/dist/comms-connections.js.map +1 -1
- package/dist/comms-lease-store.d.ts +90 -0
- package/dist/comms-lease-store.js +489 -0
- package/dist/comms-lease-store.js.map +1 -0
- package/dist/comms-receiving-evidence.d.ts +46 -0
- package/dist/comms-receiving-evidence.js +35 -0
- package/dist/comms-receiving-evidence.js.map +1 -0
- package/dist/comms-receiving-inbox.d.ts +16 -0
- package/dist/comms-receiving-inbox.js +410 -0
- package/dist/comms-receiving-inbox.js.map +1 -0
- package/dist/comms-receiving-runtime.d.ts +25 -0
- package/dist/comms-receiving-runtime.js +45 -0
- package/dist/comms-receiving-runtime.js.map +1 -0
- package/dist/comms-receiving-types.d.ts +98 -0
- package/dist/comms-receiving-types.js +2 -0
- package/dist/comms-receiving-types.js.map +1 -0
- package/dist/comms-receiving.d.ts +58 -0
- package/dist/comms-receiving.js +554 -0
- package/dist/comms-receiving.js.map +1 -0
- package/dist/comms-setup.d.ts +40 -0
- package/dist/comms-setup.js +121 -0
- package/dist/comms-setup.js.map +1 -0
- package/dist/concurrent-shared-world-lab.js +662 -624
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +6 -1
- package/dist/cua-actor-lab.js +149 -52
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/doctor-lab.js +11 -0
- package/dist/doctor-lab.js.map +1 -1
- package/dist/e2b-terminal-lab.js +3 -0
- package/dist/e2b-terminal-lab.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lab-config.d.ts +20 -3
- package/dist/lab-config.js +59 -6
- package/dist/lab-config.js.map +1 -1
- package/dist/lab-engine.js +8 -3
- package/dist/lab-engine.js.map +1 -1
- package/dist/lab-summary.d.ts +1 -0
- package/dist/lab-summary.js +8 -2
- package/dist/lab-summary.js.map +1 -1
- package/dist/observer-app.html +1 -1
- package/dist/observer-data.js +14 -1
- package/dist/observer-data.js.map +1 -1
- package/dist/oss-lab.d.ts +1 -1
- package/dist/oss-lab.js.map +1 -1
- package/dist/oss-meta-lab.d.ts +1 -1
- package/dist/oss-meta-lab.js.map +1 -1
- package/dist/program.d.ts +2 -0
- package/dist/program.js +100 -4
- package/dist/program.js.map +1 -1
- package/dist/run-narration-secrets.d.ts +5 -0
- package/dist/run-narration-secrets.js +68 -0
- package/dist/run-narration-secrets.js.map +1 -0
- package/dist/run.d.ts +8 -3
- package/dist/run.js +26 -4
- package/dist/run.js.map +1 -1
- package/dist/scripted-browser-lab.js +7 -0
- package/dist/scripted-browser-lab.js.map +1 -1
- package/dist/shared-world-lab.js +4 -0
- package/dist/shared-world-lab.js.map +1 -1
- package/dist/study-analysis-engine.js +30 -1
- package/dist/study-analysis-engine.js.map +1 -1
- package/dist/study-analysis-evidence.js +9 -1
- package/dist/study-analysis-evidence.js.map +1 -1
- package/dist/tui-app.js +134 -134
- package/dist/tui-contract.d.ts +13 -0
- package/dist/tui-contract.js.map +1 -1
- package/dist/tui-launch.d.ts +2 -0
- package/dist/tui-launch.js +9 -1
- package/dist/tui-launch.js.map +1 -1
- package/docs/architecture/comms-inbox.md +4 -0
- package/docs/architecture/real-email-receiving.md +135 -0
- package/docs/contracts/run-bundle.md +16 -5
- package/docs/contracts/schemas.md +20 -3
- package/docs/goals/current.md +5 -5
- package/docs/ramp/README.md +13 -5
- package/docs/release/0.96.0-real-email-receiving.md +47 -0
- package/docs/release/0.96.1-browser-navigation.md +22 -0
- package/package.json +2 -1
- package/skills/humanish/SKILL.md +33 -13
package/dist/cua-actor-lab.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { withTransientCommsSecrets } from "./run-narration-secrets.js";
|
|
2
|
+
import { prepareReceivingRun, receivingPublication } from "./comms-receiving-runtime.js";
|
|
3
|
+
import { deployReceivingInbox } from "./comms-receiving-inbox.js";
|
|
4
|
+
import { receivingEmailValidationReason } from "./lab-config.js";
|
|
1
5
|
// The computer-use lab backend: a subject (an app-url the caller provisioned, or a repo the
|
|
2
6
|
// lab clones AND serves in-sandbox) driven by a REGISTRY-RESOLVED computer-use actor inside a
|
|
3
7
|
// hosted E2B desktop. This is the path that makes `actors[].type` load-bearing — the
|
|
@@ -237,7 +241,7 @@ export function composeLaneInstructions(args) {
|
|
|
237
241
|
* runtime loopback/getHost address (not secret), so — mirroring the lobby-code runtime injection — this
|
|
238
242
|
* augments only the instructions the model receives; the authored prompt + its digest are unchanged.
|
|
239
243
|
* Returns a new spec (never mutates). Shared by the CUA + concurrent shared-world routes. */
|
|
240
|
-
export function withInboxMission(spec, inboxUrl, address) {
|
|
244
|
+
export function withInboxMission(spec, inboxUrl, address, receiving = false) {
|
|
241
245
|
// No assigned identity means no participant inbox; never fall back to the shared operator view.
|
|
242
246
|
if (!address?.trim())
|
|
243
247
|
return spec;
|
|
@@ -248,6 +252,11 @@ export function withInboxMission(spec, inboxUrl, address) {
|
|
|
248
252
|
// and ends its session — the exact give-up class a live run documented — unless told the wait
|
|
249
253
|
// is expected and the inbox is the next step.
|
|
250
254
|
const identity = ` Your email address is ${address} — when the app asks for an email address, enter exactly that.`;
|
|
255
|
+
if (receiving)
|
|
256
|
+
return {
|
|
257
|
+
...spec,
|
|
258
|
+
instructions: `${spec.instructions}\n\nEmail inbox:${identity} This is a fresh test identity; it does not replace an existing account's email address. When the app says it sent email, open ${inboxUrl} to check your inbox. Read the original email and use its verification link or code. Delivery may take a little time; refresh if needed. If mail remains missing or unavailable, report what you observed rather than assuming the app failed to send. The inbox may block remote images or undeclared destinations; those are harness limitations.`
|
|
259
|
+
};
|
|
251
260
|
return {
|
|
252
261
|
...spec,
|
|
253
262
|
instructions: `${spec.instructions}\n\nEmail inbox:${identity} When the app tells you it has emailed you (a verification link, confirmation code, or magic link), open ${recipientInboxUrl(inboxUrl, address)} in the browser to read that email and follow its link or enter its code. All email the app sends you arrives there. Waiting for an email is normal, not a blocker — do not end your session while waiting; open the inbox and refresh it until the email appears.`
|
|
@@ -1155,36 +1164,48 @@ function isMeasuredViewport(value) {
|
|
|
1155
1164
|
function isPositiveMeasurement(value) {
|
|
1156
1165
|
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
1157
1166
|
}
|
|
1158
|
-
async function
|
|
1167
|
+
async function measureBrowserWindowWithXwininfo(desktop, windowId, requestTimeoutMs) {
|
|
1159
1168
|
const result = await desktop.commands.run([
|
|
1160
1169
|
"set -euo pipefail",
|
|
1161
1170
|
`win=${shellSingleQuote(windowId)}`,
|
|
1162
|
-
|
|
1171
|
+
// Older xdotool builds translate parent-relative offsets twice. With window
|
|
1172
|
+
// decorations that falsely reports a visible client as clipped, triggering
|
|
1173
|
+
// fullscreen and hiding the participant's address bar. Read root-relative
|
|
1174
|
+
// client coordinates directly; never substitute emulated CDP outer bounds.
|
|
1175
|
+
"LC_ALL=C xwininfo -id \"$win\" -stats 2>/dev/null"
|
|
1163
1176
|
].join("\n"), { requestTimeoutMs, timeoutMs: 5_000 });
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1177
|
+
if (result.exitCode !== undefined && result.exitCode !== 0)
|
|
1178
|
+
return undefined;
|
|
1179
|
+
return parseXwininfoGeometry(result.stdout ?? "");
|
|
1180
|
+
}
|
|
1181
|
+
/** Root-relative physical client bounds from xwininfo's C-locale stats. */
|
|
1182
|
+
export function parseXwininfoGeometry(output) {
|
|
1183
|
+
const read = (label) => {
|
|
1184
|
+
const matches = [...output.matchAll(new RegExp(`^\\s*${label}:\\s*(-?\\d+)\\s*$`, "gm"))];
|
|
1185
|
+
if (matches.length !== 1)
|
|
1168
1186
|
return undefined;
|
|
1169
|
-
const value = Number(
|
|
1170
|
-
return Number.
|
|
1187
|
+
const value = Number(matches[0][1]);
|
|
1188
|
+
return Number.isSafeInteger(value) ? value : undefined;
|
|
1171
1189
|
};
|
|
1172
|
-
const x = read("X");
|
|
1173
|
-
const y = read("Y");
|
|
1174
|
-
const width = read("
|
|
1175
|
-
const height = read("
|
|
1190
|
+
const x = read("Absolute upper-left X");
|
|
1191
|
+
const y = read("Absolute upper-left Y");
|
|
1192
|
+
const width = read("Width");
|
|
1193
|
+
const height = read("Height");
|
|
1194
|
+
const mapStates = [...output.matchAll(/^\s*Map State:\s*(\S+)\s*$/gm)];
|
|
1195
|
+
if (mapStates.length !== 1 || mapStates[0][1] !== "IsViewable")
|
|
1196
|
+
return undefined;
|
|
1176
1197
|
if (x === undefined || y === undefined || width === undefined || height === undefined || width <= 0 || height <= 0) {
|
|
1177
1198
|
return undefined;
|
|
1178
1199
|
}
|
|
1179
|
-
return { x, y, width, height, source: "
|
|
1200
|
+
return { x, y, width, height, source: "xwininfo" };
|
|
1180
1201
|
}
|
|
1181
1202
|
/** Physical X client bounds, never the page's emulated window.outerWidth/Height. */
|
|
1182
1203
|
function isBrowserWindowContained(bounds, [width, height]) {
|
|
1183
1204
|
return bounds.x >= 0 && bounds.y >= 0
|
|
1184
1205
|
&& bounds.x + bounds.width <= width && bounds.y + bounds.height <= height;
|
|
1185
1206
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
*
|
|
1207
|
+
/** Bounded repair. Resizing can clear a window-manager maximize state and move the
|
|
1208
|
+
* client origin as decorations return, so remeasure before a second adjustment. */
|
|
1188
1209
|
async function fitBrowserWindowWithinDesktop(desktop, windowId, resolution, requestTimeoutMs) {
|
|
1189
1210
|
const run = (command) => desktop.commands.run([
|
|
1190
1211
|
"set -euo pipefail",
|
|
@@ -1193,18 +1214,22 @@ async function fitBrowserWindowWithinDesktop(desktop, windowId, resolution, requ
|
|
|
1193
1214
|
].join("\n"), { requestTimeoutMs, timeoutMs: 5_000 }).catch(() => undefined);
|
|
1194
1215
|
await run('xdotool windowmove "$win" 0 0');
|
|
1195
1216
|
await desktop.wait(250).catch(() => undefined);
|
|
1196
|
-
const moved = await
|
|
1217
|
+
const moved = await measureBrowserWindowWithXwininfo(desktop, windowId, requestTimeoutMs).catch(() => undefined);
|
|
1197
1218
|
if (moved === undefined)
|
|
1198
1219
|
return moved;
|
|
1199
1220
|
let resized = moved;
|
|
1200
|
-
const width = resolution[0] - moved.x;
|
|
1201
|
-
const height = resolution[1] - moved.y;
|
|
1202
1221
|
// Resizing alone cannot fix an offscreen client origin. The window manager
|
|
1203
1222
|
// can also center a minimum-width client at a negative x on a narrow screen.
|
|
1204
|
-
|
|
1223
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1224
|
+
if (isBrowserWindowContained(resized, resolution))
|
|
1225
|
+
return resized;
|
|
1226
|
+
const width = resolution[0] - resized.x;
|
|
1227
|
+
const height = resolution[1] - resized.y;
|
|
1228
|
+
if (resized.x < 0 || resized.y < 0 || width <= 0 || height <= 0)
|
|
1229
|
+
break;
|
|
1205
1230
|
await run(`xdotool windowsize "$win" ${width} ${height}`);
|
|
1206
1231
|
await desktop.wait(250).catch(() => undefined);
|
|
1207
|
-
const measured = await
|
|
1232
|
+
const measured = await measureBrowserWindowWithXwininfo(desktop, windowId, requestTimeoutMs).catch(() => undefined);
|
|
1208
1233
|
if (measured === undefined)
|
|
1209
1234
|
return measured;
|
|
1210
1235
|
resized = measured;
|
|
@@ -1227,7 +1252,7 @@ async function fitBrowserWindowWithinDesktop(desktop, windowId, resolution, requ
|
|
|
1227
1252
|
// Give the window manager a bounded settling window, keeping missing reads unverified.
|
|
1228
1253
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1229
1254
|
await desktop.wait(250).catch(() => undefined);
|
|
1230
|
-
const measured = await
|
|
1255
|
+
const measured = await measureBrowserWindowWithXwininfo(desktop, windowId, requestTimeoutMs).catch(() => undefined);
|
|
1231
1256
|
if (measured === undefined || isBrowserWindowContained(measured, resolution))
|
|
1232
1257
|
return measured;
|
|
1233
1258
|
resized = measured;
|
|
@@ -1244,40 +1269,40 @@ export async function captureDesktopBrowserGeometry(args) {
|
|
|
1244
1269
|
return undefined;
|
|
1245
1270
|
});
|
|
1246
1271
|
}
|
|
1247
|
-
let
|
|
1272
|
+
let physicalWindow;
|
|
1248
1273
|
if (browserWindowId !== undefined) {
|
|
1249
1274
|
if (args.resize !== false) {
|
|
1250
1275
|
await fillDesktopBrowserWindow(args.desktop, browserWindowId, args.requestedScreen, args.requestTimeoutMs);
|
|
1251
1276
|
// Let the window manager apply the resize before querying both X and page layout geometry.
|
|
1252
1277
|
await args.desktop.wait(250).catch(() => undefined);
|
|
1253
1278
|
}
|
|
1254
|
-
|
|
1279
|
+
physicalWindow = await measureBrowserWindowWithXwininfo(args.desktop, browserWindowId, args.requestTimeoutMs)
|
|
1255
1280
|
.catch(() => undefined);
|
|
1256
1281
|
}
|
|
1257
1282
|
else {
|
|
1258
1283
|
warnings.push(`Browser window bounds could not be measured for lane ${args.laneId}; the live stream will use the full desktop.`);
|
|
1259
1284
|
}
|
|
1260
1285
|
let unusable;
|
|
1261
|
-
if (
|
|
1262
|
-
const before =
|
|
1286
|
+
if (physicalWindow !== undefined && !isBrowserWindowContained(physicalWindow, args.requestedScreen)) {
|
|
1287
|
+
const before = physicalWindow;
|
|
1263
1288
|
if (args.resize !== false && browserWindowId !== undefined) {
|
|
1264
|
-
|
|
1265
|
-
if (
|
|
1289
|
+
physicalWindow = await fitBrowserWindowWithinDesktop(args.desktop, browserWindowId, args.requestedScreen, args.requestTimeoutMs);
|
|
1290
|
+
if (physicalWindow === undefined) {
|
|
1266
1291
|
// Keep the last measured bad state; a missing observation cannot prove a successful fix.
|
|
1267
|
-
|
|
1292
|
+
physicalWindow = before;
|
|
1268
1293
|
unusable = `Physical browser containment could not be verified after correction for lane ${args.laneId}; the last measured window was clipped.`;
|
|
1269
1294
|
}
|
|
1270
|
-
else if (isBrowserWindowContained(
|
|
1271
|
-
warnings.push(`Browser window clipping corrected for lane ${args.laneId}; physical bounds are ${
|
|
1295
|
+
else if (isBrowserWindowContained(physicalWindow, args.requestedScreen)) {
|
|
1296
|
+
warnings.push(`Browser window clipping corrected for lane ${args.laneId}; physical bounds are ${physicalWindow.width}x${physicalWindow.height} at (${physicalWindow.x}, ${physicalWindow.y}).`);
|
|
1272
1297
|
}
|
|
1273
1298
|
}
|
|
1274
|
-
if (unusable === undefined && !isBrowserWindowContained(
|
|
1275
|
-
unusable = `Browser window is outside the captured ${args.requestedScreen[0]}x${args.requestedScreen[1]} desktop for lane ${args.laneId}: physical bounds ${
|
|
1299
|
+
if (unusable === undefined && !isBrowserWindowContained(physicalWindow, args.requestedScreen)) {
|
|
1300
|
+
unusable = `Browser window is outside the captured ${args.requestedScreen[0]}x${args.requestedScreen[1]} desktop for lane ${args.laneId}: physical bounds ${physicalWindow.width}x${physicalWindow.height} at (${physicalWindow.x}, ${physicalWindow.y}), right=${physicalWindow.x + physicalWindow.width}, bottom=${physicalWindow.y + physicalWindow.height}.`;
|
|
1276
1301
|
}
|
|
1277
1302
|
if (unusable !== undefined)
|
|
1278
1303
|
warnings.push(unusable);
|
|
1279
1304
|
}
|
|
1280
|
-
if (
|
|
1305
|
+
if (physicalWindow === undefined) {
|
|
1281
1306
|
warnings.push(`Physical browser containment is unverified for lane ${args.laneId}; X window bounds could not be measured. Page-reported outer dimensions can be emulated and do not prove physical visibility.`);
|
|
1282
1307
|
}
|
|
1283
1308
|
let cdpUnavailable;
|
|
@@ -1293,11 +1318,11 @@ export async function captureDesktopBrowserGeometry(args) {
|
|
|
1293
1318
|
return undefined;
|
|
1294
1319
|
})
|
|
1295
1320
|
: undefined;
|
|
1296
|
-
const browserWindow =
|
|
1321
|
+
const browserWindow = physicalWindow ?? chromeGeometry?.browserWindow;
|
|
1297
1322
|
const viewport = chromeGeometry?.viewport;
|
|
1298
1323
|
// The fill check reads the X window when it was measured: under mobile emulation (#221) the
|
|
1299
1324
|
// page's window.outerWidth reports the EMULATED screen (414), which is not a fill failure.
|
|
1300
|
-
const fillBounds =
|
|
1325
|
+
const fillBounds = physicalWindow;
|
|
1301
1326
|
if (!browserWindow) {
|
|
1302
1327
|
warnings.push(`Browser outer bounds could not be measured for lane ${args.laneId}.`);
|
|
1303
1328
|
}
|
|
@@ -1653,12 +1678,13 @@ export async function runCuaLane(spec, deps) {
|
|
|
1653
1678
|
// in-sandbox catch (loopback) so its verification mail is CAPTURED, not sent to the internet. Gated
|
|
1654
1679
|
// ENTIRELY on config.comms — no comms declared → zero change. The base-URL env is injected at
|
|
1655
1680
|
// sandbox-create (below, so the app reads it at boot); the catch is started right after create.
|
|
1656
|
-
const commsEmail = (cloneRoute || localTreeRoute)
|
|
1681
|
+
const commsEmail = (cloneRoute || localTreeRoute) && config.comms?.email?.kind === "fake" ? config.comms.email : undefined;
|
|
1657
1682
|
const commsPort = commsEmail ? (commsEmail.port ?? DEFAULT_SANDBOX_CATCH_PORT) : undefined;
|
|
1658
1683
|
// Hoisted so the finally can drain the catch before teardown; `commsArtifactPath` is the written
|
|
1659
1684
|
// evidence path folded into the lane outcome.
|
|
1660
1685
|
let deployedComms;
|
|
1661
1686
|
let commsArtifactPath;
|
|
1687
|
+
let receivingInboxUrl;
|
|
1662
1688
|
// injectEnv is absent on an adopter-hosted plane (#328): there is no subject env to inject
|
|
1663
1689
|
// because the operator points their own app at their own catch.
|
|
1664
1690
|
const commsEnv = commsEmail?.injectEnv !== undefined && commsPort !== undefined
|
|
@@ -1810,6 +1836,27 @@ export async function runCuaLane(spec, deps) {
|
|
|
1810
1836
|
// Start the in-sandbox email catch BEFORE the subject serve, so the app's send-API base URL (injected
|
|
1811
1837
|
// into its env at create) resolves the moment it boots. A comms-declared lab that can't stand the
|
|
1812
1838
|
// catch up is a setup failure (fail closed) rather than silently sending real mail.
|
|
1839
|
+
if (deps.receiving) {
|
|
1840
|
+
const surface = await deployReceivingInbox(desktop, { leaseId: spec.streamId, requestTimeoutMs: Math.min(deps.requestTimeoutMs, 30_000) });
|
|
1841
|
+
receivingInboxUrl = surface.url;
|
|
1842
|
+
const email = config.comms?.email;
|
|
1843
|
+
try {
|
|
1844
|
+
await deps.receiving.attach(spec.laneId, {
|
|
1845
|
+
surface,
|
|
1846
|
+
allowedOrigins: [...new Set([new URL(targetUrl).origin, ...(email?.allowedOrigins ?? [])])],
|
|
1847
|
+
originMap: buildOriginMap({
|
|
1848
|
+
...(config.subject.serve?.url === undefined ? {} : { internalServeUrl: config.subject.serve.url }),
|
|
1849
|
+
reachableBaseUrl: targetUrl,
|
|
1850
|
+
...(email?.linkOrigin === undefined ? {} : { linkOrigin: email.linkOrigin })
|
|
1851
|
+
})
|
|
1852
|
+
});
|
|
1853
|
+
commsArtifactPath = "comms/receiving.json";
|
|
1854
|
+
}
|
|
1855
|
+
catch (error) {
|
|
1856
|
+
await surface.stop().catch(() => { });
|
|
1857
|
+
throw error;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1813
1860
|
if (commsEmail && commsPort !== undefined) {
|
|
1814
1861
|
deployedComms = await deployCommsCatch(desktop, {
|
|
1815
1862
|
port: commsPort,
|
|
@@ -2098,11 +2145,13 @@ export async function runCuaLane(spec, deps) {
|
|
|
2098
2145
|
// recipient it can actually receive mail into (else it would stall on an inbox that stays
|
|
2099
2146
|
// empty). Two comms planes, mutually exclusive by parse: the in-sandbox catch humanish
|
|
2100
2147
|
// deployed, or the adopter-hosted one (#380).
|
|
2101
|
-
instructions:
|
|
2102
|
-
? withInboxMission(spec,
|
|
2103
|
-
:
|
|
2104
|
-
? withInboxMission(spec,
|
|
2105
|
-
: spec.
|
|
2148
|
+
instructions: deps.receiving && receivingInboxUrl
|
|
2149
|
+
? withInboxMission(spec, receivingInboxUrl, deps.receiving.address(spec.laneId), true).instructions
|
|
2150
|
+
: commsEmail && commsInboxUrl && deployedComms?.ready && laneHasInboxRecipient(commsEmail, spec.laneId)
|
|
2151
|
+
? withInboxMission(spec, commsInboxUrl, inboxRecipientFor(commsEmail, spec.laneId)?.address).instructions
|
|
2152
|
+
: deps.externalComms && laneHasInboxRecipient(deps.externalComms.email, spec.laneId)
|
|
2153
|
+
? withInboxMission(spec, deps.externalComms.inboxUrl, inboxRecipientFor(deps.externalComms.email, spec.laneId)?.address).instructions
|
|
2154
|
+
: spec.instructions,
|
|
2106
2155
|
persona: spec.persona,
|
|
2107
2156
|
timeoutMs: deps.timeoutMs,
|
|
2108
2157
|
// The brain is either a keyed API client or a CLI the operator is already signed in to.
|
|
@@ -2257,6 +2306,14 @@ export async function runCuaLane(spec, deps) {
|
|
|
2257
2306
|
: { warnings: [...(desktopGeometry.warnings ?? []), ...geometryWarnings] })
|
|
2258
2307
|
};
|
|
2259
2308
|
}
|
|
2309
|
+
if (deps.receiving) {
|
|
2310
|
+
try {
|
|
2311
|
+
await deps.receiving.finishParticipant(spec.laneId);
|
|
2312
|
+
}
|
|
2313
|
+
catch {
|
|
2314
|
+
warnings.push("Real email finalization is incomplete. Inspect communication cleanup with humanish comms recover.");
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2260
2317
|
// Off-app comms evidence (#297): before this lane's sandbox is torn down, drain everything the
|
|
2261
2318
|
// in-sandbox catch captured, route it into a host fake inbox addressed to the declared
|
|
2262
2319
|
// recipients, and write the digest-only thread artifact. Wrapped so a drain failure NEVER
|
|
@@ -2700,6 +2757,9 @@ function subjectProvenanceArg(subject, publicRepo, subjectEnvNames) {
|
|
|
2700
2757
|
* ticking into a directory something else is deleting, which surfaces as an unrelated ENOTEMPTY.
|
|
2701
2758
|
*/
|
|
2702
2759
|
export async function runCuaActorLab(options) {
|
|
2760
|
+
return withTransientCommsSecrets(() => runCuaActorLabWithSecrets(options));
|
|
2761
|
+
}
|
|
2762
|
+
async function runCuaActorLabWithSecrets(options) {
|
|
2703
2763
|
const analysisReason = resolveAutomaticAnalysis(options.config.review?.analysis);
|
|
2704
2764
|
const tasksReason = analysisReason.ok ? taskProtocolValidationReason(options.config, true) : analysisReason.message;
|
|
2705
2765
|
if (tasksReason)
|
|
@@ -2792,7 +2852,12 @@ async function runCuaActorLabInScope(options) {
|
|
|
2792
2852
|
if (actor?.maxOutputTokens !== undefined && (hooks.runSession || hooks.buildProvider || hooks.buildExecutor)) {
|
|
2793
2853
|
return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "maxOutputTokens cannot be enforced by a custom runSession/provider/executor route.", descriptor.id);
|
|
2794
2854
|
}
|
|
2855
|
+
const receivingReason = receivingEmailValidationReason(config);
|
|
2856
|
+
if (receivingReason)
|
|
2857
|
+
return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", receivingReason, descriptor.id);
|
|
2795
2858
|
const inProcessRoute = hooks.buildExecutor !== undefined;
|
|
2859
|
+
if (inProcessRoute && config.comms?.email?.kind === "real")
|
|
2860
|
+
return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "Real email receiving requires hosted participant desktops.", descriptor.id);
|
|
2796
2861
|
if (inProcessRoute && config.execution?.desktop?.media !== undefined) {
|
|
2797
2862
|
return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "execution.desktop.media is not provisioned by a caller-supplied executor. Remove the declaration or use a hosted computer-use browser lane.", descriptor.id);
|
|
2798
2863
|
}
|
|
@@ -3267,20 +3332,47 @@ async function runCuaActorLabInScope(options) {
|
|
|
3267
3332
|
await flushWriting;
|
|
3268
3333
|
};
|
|
3269
3334
|
}
|
|
3335
|
+
const receivingWarnings = [];
|
|
3336
|
+
let receiving;
|
|
3337
|
+
if (!dryRun && config.comms?.email?.kind === "real") {
|
|
3338
|
+
try {
|
|
3339
|
+
receiving = await prepareReceivingRun({ cwd, runId, config, env, participants: laneSpecs.map(spec => spec.laneId), runPaths,
|
|
3340
|
+
registerSecrets: values => { for (const value of values)
|
|
3341
|
+
if (value.length >= 4 && !knownSecretValues.includes(value))
|
|
3342
|
+
knownSecretValues.push(value); }
|
|
3343
|
+
});
|
|
3344
|
+
if (receiving)
|
|
3345
|
+
deps.receiving = receiving;
|
|
3346
|
+
}
|
|
3347
|
+
catch {
|
|
3348
|
+
await stopLiveFlush?.();
|
|
3349
|
+
return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "Real email setup failed before desktop allocation. Run humanish comms check --online and humanish comms recover to inspect authentication and pending cleanup.", descriptor.id);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3270
3352
|
// Run lanes (dry-run runs none). In-process is always one lane.
|
|
3271
3353
|
let outcomes;
|
|
3272
3354
|
let failFastReason;
|
|
3273
|
-
|
|
3274
|
-
if (
|
|
3275
|
-
|
|
3355
|
+
try {
|
|
3356
|
+
if (!dryRun) {
|
|
3357
|
+
if (inProcessRoute) {
|
|
3358
|
+
outcomes = [await runInProcessLane(laneSpecs[0], deps)];
|
|
3359
|
+
}
|
|
3360
|
+
else if (laneCount === 1) {
|
|
3361
|
+
outcomes = [await runCuaLane(laneSpecs[0], deps)];
|
|
3362
|
+
}
|
|
3363
|
+
else {
|
|
3364
|
+
const ran = await runCuaLanes(laneSpecs, deps, plan.concurrency);
|
|
3365
|
+
outcomes = ran.outcomes;
|
|
3366
|
+
failFastReason = ran.failFastReason;
|
|
3367
|
+
}
|
|
3276
3368
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3369
|
+
}
|
|
3370
|
+
finally {
|
|
3371
|
+
try {
|
|
3372
|
+
await receiving?.finish();
|
|
3279
3373
|
}
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
outcomes = ran.outcomes;
|
|
3283
|
-
failFastReason = ran.failFastReason;
|
|
3374
|
+
catch {
|
|
3375
|
+
receivingWarnings.push("Email finalization could not complete. Inspect humanish comms recover; provider cleanup remains unresolved.");
|
|
3284
3376
|
}
|
|
3285
3377
|
}
|
|
3286
3378
|
// Close the live flush BEFORE any final artifact work: no new flush may start, and an
|
|
@@ -3438,6 +3530,8 @@ async function runCuaActorLabInScope(options) {
|
|
|
3438
3530
|
hookLabel: "cuaHooks",
|
|
3439
3531
|
...(options.scorerProvenance === undefined ? {} : { scorerProvenance: options.scorerProvenance })
|
|
3440
3532
|
});
|
|
3533
|
+
if (receiving)
|
|
3534
|
+
bundle.commsReceiving = receiving.snapshot();
|
|
3441
3535
|
await writeCuaRunArtifacts(bundle, createdAt, runPaths);
|
|
3442
3536
|
// Finalize the status record from the bundle that was just written, so the index can never
|
|
3443
3537
|
// claim an outcome the evidence does not carry. A run that throws before reaching here leaves
|
|
@@ -3467,7 +3561,7 @@ async function runCuaActorLabInScope(options) {
|
|
|
3467
3561
|
const adapterFailure = adapterScoreFailureMessage(bundle);
|
|
3468
3562
|
const ok = observer.ok && allLanesOk && adapterFailure === undefined && scorerResult.declaredVerdictFailure === undefined;
|
|
3469
3563
|
const laneWarnings = (outcomes ?? []).flatMap((outcome) => outcome.warnings);
|
|
3470
|
-
const warnings = [...laneWarnings, ...aggregateWarnings, ...adapterWarnings, ...observer.warnings];
|
|
3564
|
+
const warnings = [...receivingWarnings, ...laneWarnings, ...aggregateWarnings, ...adapterWarnings, ...observer.warnings];
|
|
3471
3565
|
const laneResults = laneSpecs.map((spec, index) => toLaneResult(spec, outcomes?.[index], laneSubjects[index], dryRun));
|
|
3472
3566
|
const laneSummary = buildLaneSummary(outcomes, laneCount, plan, dryRun);
|
|
3473
3567
|
const firstOutcome = outcomes?.[0];
|
|
@@ -3625,6 +3719,7 @@ function observerResultForCuaArtifacts(cwd, runId, artifactRoot, warnings = [])
|
|
|
3625
3719
|
function buildSingleLaneBundle(args) {
|
|
3626
3720
|
const { spec, outcome, config } = args;
|
|
3627
3721
|
return buildCuaBundle({
|
|
3722
|
+
realEmail: config.comms?.email?.kind === "real",
|
|
3628
3723
|
...(args.lab === undefined ? {} : { lab: args.lab }),
|
|
3629
3724
|
actorId: args.descriptor.id,
|
|
3630
3725
|
appUrl: args.appUrl,
|
|
@@ -4520,6 +4615,7 @@ export function buildCuaBundle(args) {
|
|
|
4520
4615
|
}, [stream]);
|
|
4521
4616
|
return {
|
|
4522
4617
|
schema: RUN_BUNDLE_SCHEMA,
|
|
4618
|
+
...(args.realEmail && !args.dryRun ? { publication: { restrictions: ["real-communications"] } } : {}),
|
|
4523
4619
|
runId: args.runId,
|
|
4524
4620
|
mode: args.dryRun ? "dry-run" : "live",
|
|
4525
4621
|
simCount: 1,
|
|
@@ -5008,6 +5104,7 @@ export function buildCuaFanoutBundle(args) {
|
|
|
5008
5104
|
const cost = buildCuaCostSummary({ lanes: costLanes, desktops });
|
|
5009
5105
|
return {
|
|
5010
5106
|
schema: RUN_BUNDLE_SCHEMA,
|
|
5107
|
+
...receivingPublication(args.config, args.dryRun),
|
|
5011
5108
|
runId: args.runId,
|
|
5012
5109
|
mode: args.dryRun ? "dry-run" : "live",
|
|
5013
5110
|
simCount: specs.length,
|