humanish 0.15.2 → 0.16.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/README.md +15 -7
- package/dist/actor-contract.d.ts +2 -3
- package/dist/actor-contract.js +6 -7
- package/dist/actor-contract.js.map +1 -1
- package/dist/actor-registry.d.ts +4 -4
- package/dist/actor-registry.js +8 -7
- package/dist/actor-registry.js.map +1 -1
- package/dist/artifact-reference.js +1 -1
- package/dist/artifact-reference.js.map +1 -1
- package/dist/concurrent-shared-world-lab.js +24 -11
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +101 -4
- package/dist/cua-actor-lab.js +456 -77
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/device-presets.d.ts +4 -4
- package/dist/device-presets.js +5 -5
- package/dist/device-presets.js.map +1 -1
- package/dist/e2b-terminal-lab.d.ts +18 -20
- package/dist/e2b-terminal-lab.js +28 -28
- package/dist/e2b-terminal-lab.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lab-config.d.ts +27 -28
- package/dist/lab-config.js +6 -6
- package/dist/lab-config.js.map +1 -1
- package/dist/lab-engine.d.ts +1 -1
- package/dist/lab-engine.js +8 -10
- package/dist/lab-engine.js.map +1 -1
- package/dist/observer-assets.js +92 -15
- package/dist/observer-assets.js.map +1 -1
- package/dist/oss-meta-lab.js +2 -4
- package/dist/oss-meta-lab.js.map +1 -1
- package/dist/program.js +1 -1
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +38 -0
- package/dist/run.js +71 -1
- package/dist/run.js.map +1 -1
- package/dist/shared-world-lab.d.ts +20 -1
- package/dist/shared-world-lab.js +197 -23
- package/dist/shared-world-lab.js.map +1 -1
- package/dist/terminal-agent-actor.d.ts +12 -7
- package/dist/terminal-agent-actor.js +18 -16
- package/dist/terminal-agent-actor.js.map +1 -1
- package/docs/architecture/actor-contract.md +32 -21
- package/docs/architecture/observer.md +15 -9
- package/docs/architecture/state-driven-executor.md +3 -3
- package/docs/architecture/terminal-product-lane.md +29 -25
- package/docs/assets/humanish-drawdb-hero.png +0 -0
- package/docs/contracts/adapter-fixtures.md +4 -2
- package/docs/contracts/core.md +9 -4
- package/docs/contracts/feedback.md +4 -2
- package/docs/contracts/policy.md +5 -3
- package/docs/contracts/run-bundle.md +24 -3
- package/docs/contracts/schemas.md +34 -23
- package/docs/goals/current.md +69 -20
- package/docs/ramp/README.md +32 -13
- package/docs/release/open-source-readiness.md +5 -4
- package/docs/release/public-readiness-standard.md +6 -1
- package/package.json +1 -1
package/dist/cua-actor-lab.js
CHANGED
|
@@ -58,7 +58,7 @@ const DEFAULT_SESSION_TIMEOUT_MS = 300_000;
|
|
|
58
58
|
// Settle after opening the browser, before the first screenshot — long enough for a cold
|
|
59
59
|
// browser + page load to paint (2s captured a blank desktop; the render empirically needs ~6-9s).
|
|
60
60
|
const BROWSER_SETTLE_MS = 8_000;
|
|
61
|
-
// Device/
|
|
61
|
+
// Device/screen size comes from the named-preset registry (device-presets.ts), selectable per run
|
|
62
62
|
// via execution.desktop.device (default `desktop`=1440x950). NOTE: this is run-wide for now; a
|
|
63
63
|
// per-PERSONA device dimension (N personas × devices, as the bespoke sims author) lands with
|
|
64
64
|
// fan-out. On this E2B-desktop route only width/height physically render — isMobile/DSF are
|
|
@@ -403,31 +403,34 @@ export function makeLaneWriteScreenshot(artifactRoot, spec, screenshots) {
|
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
405
|
/**
|
|
406
|
-
* Verify the desktop geometry IN-SANDBOX (the per-lane device claim is checked, never
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
406
|
+
* Verify the desktop screen geometry IN-SANDBOX (the per-lane device claim is checked, never
|
|
407
|
+
* assumed). A parseable mismatch fails closed. Unavailable/unparseable evidence is returned as
|
|
408
|
+
* an explicit warning: the lane may still run, but its bundle records only the requested screen
|
|
409
|
+
* and never upgrades that request into a verified measurement.
|
|
410
410
|
*/
|
|
411
|
-
async function
|
|
411
|
+
export async function inspectDesktopScreenGeometry(args) {
|
|
412
412
|
let out = "";
|
|
413
413
|
try {
|
|
414
|
-
const result = await desktop.commands.run("xdpyinfo 2>/dev/null | grep -i dimensions || true", { requestTimeoutMs });
|
|
414
|
+
const result = await args.desktop.commands.run("xdpyinfo 2>/dev/null | grep -i dimensions || true", { requestTimeoutMs: args.requestTimeoutMs });
|
|
415
415
|
out = (result.stdout ?? "").trim();
|
|
416
416
|
}
|
|
417
417
|
catch {
|
|
418
|
-
return
|
|
418
|
+
return { warning: `Desktop screen geometry could not be measured for lane ${args.laneId}; requested geometry remains unverified.` };
|
|
419
419
|
}
|
|
420
420
|
const match = out.match(/(\d+)\s*x\s*(\d+)\s*pixels/i);
|
|
421
421
|
if (!match) {
|
|
422
|
-
return
|
|
422
|
+
return { warning: `Desktop screen geometry could not be parsed for lane ${args.laneId}; requested geometry remains unverified.` };
|
|
423
423
|
}
|
|
424
424
|
const width = Number(match[1]);
|
|
425
425
|
const height = Number(match[2]);
|
|
426
|
-
const [expectedWidth, expectedHeight] =
|
|
426
|
+
const [expectedWidth, expectedHeight] = args.requestedScreen;
|
|
427
427
|
if (width === expectedWidth && height === expectedHeight) {
|
|
428
|
-
return
|
|
428
|
+
return { verified: { width, height, source: "xdpyinfo" } };
|
|
429
429
|
}
|
|
430
|
-
return
|
|
430
|
+
return {
|
|
431
|
+
verified: { width, height, source: "xdpyinfo" },
|
|
432
|
+
error: `HUMANISH_CUA_LAB_DEVICE_GEOMETRY: lane ${args.laneId} requested a ${expectedWidth}x${expectedHeight} desktop but xdpyinfo reports ${width}x${height} in-sandbox; the per-lane device geometry is unverified (fail-closed).`
|
|
433
|
+
};
|
|
431
434
|
}
|
|
432
435
|
/** A blocked lane outcome (pipeline gate / fail-fast skipped it before it ran). */
|
|
433
436
|
function blockedLaneOutcome(spec, reason) {
|
|
@@ -445,27 +448,58 @@ function blockedLaneOutcome(spec, reason) {
|
|
|
445
448
|
harnessError: false
|
|
446
449
|
};
|
|
447
450
|
}
|
|
448
|
-
async function findVisibleBrowserWindowId(desktop, requestTimeoutMs) {
|
|
451
|
+
async function findVisibleBrowserWindowId(desktop, requestTimeoutMs, browserFamily, launchIdentity) {
|
|
452
|
+
if (browserFamily === "unknown")
|
|
453
|
+
return undefined;
|
|
454
|
+
// The candidate loop keeps the LAST identity match: with a launch identity the match is
|
|
455
|
+
// unique anyway, and without one every family candidate matches, so the newest visible
|
|
456
|
+
// window of the launched family wins (the window this lane just opened).
|
|
457
|
+
const finder = browserFamily === "firefox"
|
|
458
|
+
? [
|
|
459
|
+
"find_firefox_window() {",
|
|
460
|
+
" timeout 2s xdotool search --onlyvisible --class 'firefox|Firefox' 2>/dev/null || true",
|
|
461
|
+
"}",
|
|
462
|
+
"window_id=",
|
|
463
|
+
"for _ in $(seq 1 10); do",
|
|
464
|
+
" for candidate in $(find_firefox_window); do",
|
|
465
|
+
" window_pid=\"$(xdotool getwindowpid \"$candidate\" 2>/dev/null || true)\"",
|
|
466
|
+
" if matches_launch_identity \"$window_pid\"; then window_id=\"$candidate\"; fi",
|
|
467
|
+
" done",
|
|
468
|
+
" if [ -n \"$window_id\" ]; then break; fi",
|
|
469
|
+
" sleep 0.5",
|
|
470
|
+
"done"
|
|
471
|
+
]
|
|
472
|
+
: [
|
|
473
|
+
"find_chrome_window() {",
|
|
474
|
+
" timeout 2s xdotool search --onlyvisible --class 'google-chrome|Google-chrome|chromium|Chromium|chrome|Chrome' 2>/dev/null || true",
|
|
475
|
+
"}",
|
|
476
|
+
"window_id=",
|
|
477
|
+
"for _ in $(seq 1 10); do",
|
|
478
|
+
" for candidate in $(find_chrome_window); do",
|
|
479
|
+
" window_pid=\"$(xdotool getwindowpid \"$candidate\" 2>/dev/null || true)\"",
|
|
480
|
+
" if matches_launch_identity \"$window_pid\"; then window_id=\"$candidate\"; fi",
|
|
481
|
+
" done",
|
|
482
|
+
" if [ -n \"$window_id\" ]; then break; fi",
|
|
483
|
+
" sleep 0.5",
|
|
484
|
+
"done"
|
|
485
|
+
];
|
|
449
486
|
const result = await desktop.commands.run([
|
|
450
487
|
"set -euo pipefail",
|
|
451
488
|
"export DISPLAY=\"${DISPLAY:-:0}\"",
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
"
|
|
455
|
-
"
|
|
456
|
-
"
|
|
457
|
-
"
|
|
458
|
-
"
|
|
459
|
-
"
|
|
489
|
+
`launch_pid=${shellSingleQuote(launchIdentity?.processId ?? "")}`,
|
|
490
|
+
`profile_dir=${shellSingleQuote(launchIdentity?.profileDir ?? "")}`,
|
|
491
|
+
"matches_launch_identity() {",
|
|
492
|
+
" if [ -z \"$launch_pid\" ] && [ -z \"$profile_dir\" ]; then return 0; fi",
|
|
493
|
+
" local current=\"${1:-}\"",
|
|
494
|
+
" while [[ \"$current\" =~ ^[0-9]+$ ]] && [ \"$current\" -gt 1 ]; do",
|
|
495
|
+
" cmdline=\"$(tr '\\0' ' ' < \"/proc/$current/cmdline\" 2>/dev/null || true)\"",
|
|
496
|
+
" if [ -n \"$profile_dir\" ] && [[ \"$cmdline\" == *\"$profile_dir\"* ]]; then return 0; fi",
|
|
497
|
+
" if [ \"$current\" = \"$launch_pid\" ]; then return 0; fi",
|
|
498
|
+
" current=\"$(ps -o ppid= -p \"$current\" 2>/dev/null | tr -d ' ' || true)\"",
|
|
499
|
+
" done",
|
|
500
|
+
" return 1",
|
|
460
501
|
"}",
|
|
461
|
-
|
|
462
|
-
"for _ in $(seq 1 10); do",
|
|
463
|
-
" window_id=\"$(find_chrome_window)\"",
|
|
464
|
-
" if [ -z \"$window_id\" ]; then window_id=\"$(find_firefox_window)\"; fi",
|
|
465
|
-
" if [ -z \"$window_id\" ]; then window_id=\"$(find_named_window)\"; fi",
|
|
466
|
-
" if [ -n \"$window_id\" ]; then break; fi",
|
|
467
|
-
" sleep 0.5",
|
|
468
|
-
"done",
|
|
502
|
+
...finder,
|
|
469
503
|
"if [ -n \"$window_id\" ]; then printf 'WINDOW_ID=%s\\n' \"$window_id\"; fi"
|
|
470
504
|
].join("\n"), {
|
|
471
505
|
requestTimeoutMs,
|
|
@@ -511,9 +545,10 @@ async function openDesktopBrowserTarget(desktop, targetUrl, requestTimeoutMs, br
|
|
|
511
545
|
"set -euo pipefail",
|
|
512
546
|
`target_url=${shellSingleQuote(targetUrl)}`,
|
|
513
547
|
`browser_preference=${shellSingleQuote(requestedBrowser)}`,
|
|
514
|
-
"chrome_profile_dir
|
|
548
|
+
"chrome_profile_dir=",
|
|
515
549
|
`chrome_preferences_json=${shellSingleQuote(chromiumEvidenceProfilePreferencesJson())}`,
|
|
516
550
|
"prepare_chrome_profile() {",
|
|
551
|
+
" chrome_profile_dir=\"$(mktemp -d /tmp/humanish-chrome-profile.XXXXXX)\"",
|
|
517
552
|
" mkdir -p \"$chrome_profile_dir/Default\"",
|
|
518
553
|
" printf '%s\\n' \"$chrome_preferences_json\" > \"$chrome_profile_dir/Default/Preferences\"",
|
|
519
554
|
"}",
|
|
@@ -523,38 +558,53 @@ async function openDesktopBrowserTarget(desktop, targetUrl, requestTimeoutMs, br
|
|
|
523
558
|
" shift 2",
|
|
524
559
|
" if command -v \"$binary\" >/dev/null 2>&1; then",
|
|
525
560
|
" nohup \"$binary\" \"$@\" \"$target_url\" >/tmp/humanish-browser-open.log 2>&1 &",
|
|
561
|
+
" local launch_pid=$!",
|
|
526
562
|
" printf 'HUMANISH_BROWSER_RESOLVED=%s\\n' \"$label\"",
|
|
563
|
+
" printf 'HUMANISH_BROWSER_PID=%s\\n' \"$launch_pid\"",
|
|
564
|
+
" printf 'HUMANISH_BROWSER_PROFILE_DIR=%s\\n' \"$chrome_profile_dir\"",
|
|
565
|
+
" if [[ \"$label\" =~ ^(google-chrome|google-chrome-stable|chromium|chromium-browser)$ ]]; then",
|
|
566
|
+
" for _ in $(seq 1 30); do",
|
|
567
|
+
" if [ -s \"$chrome_profile_dir/DevToolsActivePort\" ]; then",
|
|
568
|
+
" head -n 1 \"$chrome_profile_dir/DevToolsActivePort\" | sed 's/^/HUMANISH_BROWSER_CDP_PORT=/'",
|
|
569
|
+
" break",
|
|
570
|
+
" fi",
|
|
571
|
+
" sleep 0.1",
|
|
572
|
+
" done",
|
|
573
|
+
" fi",
|
|
527
574
|
" return 0",
|
|
528
575
|
" fi",
|
|
529
576
|
" return 1",
|
|
530
577
|
"}",
|
|
531
|
-
`chrome_debug_flags=(--remote-debugging-address=127.0.0.1 --remote-debugging-port=
|
|
532
|
-
"prepare_chrome_profile",
|
|
578
|
+
`chrome_debug_flags=(--remote-debugging-address=127.0.0.1 --remote-debugging-port=0 ${chromiumFlags})`,
|
|
533
579
|
"open_target() {",
|
|
534
580
|
" case \"$browser_preference\" in",
|
|
535
581
|
" chrome)",
|
|
536
|
-
"
|
|
537
|
-
" launch_browser google-chrome
|
|
582
|
+
" prepare_chrome_profile",
|
|
583
|
+
" launch_browser google-chrome google-chrome --new-window \"--user-data-dir=$chrome_profile_dir\" \"${chrome_debug_flags[@]}\" && return 0",
|
|
584
|
+
" launch_browser google-chrome-stable google-chrome-stable --new-window \"--user-data-dir=$chrome_profile_dir\" \"${chrome_debug_flags[@]}\" && return 0",
|
|
538
585
|
" echo 'requested browser chrome was not found' >&2",
|
|
539
586
|
" return 127",
|
|
540
587
|
" ;;",
|
|
541
588
|
" chromium)",
|
|
542
|
-
"
|
|
543
|
-
" launch_browser chromium
|
|
589
|
+
" prepare_chrome_profile",
|
|
590
|
+
" launch_browser chromium chromium --new-window \"--user-data-dir=$chrome_profile_dir\" \"${chrome_debug_flags[@]}\" && return 0",
|
|
591
|
+
" launch_browser chromium-browser chromium-browser --new-window \"--user-data-dir=$chrome_profile_dir\" \"${chrome_debug_flags[@]}\" && return 0",
|
|
544
592
|
" echo 'requested browser chromium was not found' >&2",
|
|
545
593
|
" return 127",
|
|
546
594
|
" ;;",
|
|
547
595
|
" firefox)",
|
|
548
|
-
"
|
|
596
|
+
" prepare_chrome_profile",
|
|
597
|
+
" launch_browser firefox firefox --new-instance --no-remote --new-window --profile \"$chrome_profile_dir\" && return 0",
|
|
549
598
|
" echo 'requested browser firefox was not found' >&2",
|
|
550
599
|
" return 127",
|
|
551
600
|
" ;;",
|
|
552
601
|
" default)",
|
|
553
|
-
"
|
|
554
|
-
" launch_browser google-chrome
|
|
555
|
-
" launch_browser
|
|
556
|
-
" launch_browser chromium
|
|
557
|
-
" launch_browser
|
|
602
|
+
" prepare_chrome_profile",
|
|
603
|
+
" launch_browser google-chrome google-chrome --new-window \"--user-data-dir=$chrome_profile_dir\" \"${chrome_debug_flags[@]}\" && return 0",
|
|
604
|
+
" launch_browser google-chrome-stable google-chrome-stable --new-window \"--user-data-dir=$chrome_profile_dir\" \"${chrome_debug_flags[@]}\" && return 0",
|
|
605
|
+
" launch_browser chromium chromium --new-window \"--user-data-dir=$chrome_profile_dir\" \"${chrome_debug_flags[@]}\" && return 0",
|
|
606
|
+
" launch_browser chromium-browser chromium-browser --new-window \"--user-data-dir=$chrome_profile_dir\" \"${chrome_debug_flags[@]}\" && return 0",
|
|
607
|
+
" launch_browser firefox firefox --new-instance --no-remote --new-window --profile \"$chrome_profile_dir\" && return 0",
|
|
558
608
|
" launch_browser xdg-open xdg-open && return 0",
|
|
559
609
|
" echo 'no browser opener found' >&2",
|
|
560
610
|
" return 127",
|
|
@@ -571,9 +621,18 @@ async function openDesktopBrowserTarget(desktop, targetUrl, requestTimeoutMs, br
|
|
|
571
621
|
throw new Error(`browser launch failed with exit ${result.exitCode}: ${tailOf(result.stderr ?? result.stdout ?? "")}`);
|
|
572
622
|
}
|
|
573
623
|
const resolved = (result.stdout ?? "").match(/^HUMANISH_BROWSER_RESOLVED=(\S+)$/m)?.[1];
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
624
|
+
const processId = (result.stdout ?? "").match(/^HUMANISH_BROWSER_PID=(\d+)$/m)?.[1];
|
|
625
|
+
const profileDir = (result.stdout ?? "").match(/^HUMANISH_BROWSER_PROFILE_DIR=(\S+)$/m)?.[1];
|
|
626
|
+
const cdpPortRaw = (result.stdout ?? "").match(/^HUMANISH_BROWSER_CDP_PORT=(\d+)$/m)?.[1];
|
|
627
|
+
const cdpPort = cdpPortRaw === undefined ? undefined : Number(cdpPortRaw);
|
|
628
|
+
return {
|
|
629
|
+
family: desktopBrowserFamily(resolved ?? requestedBrowser),
|
|
630
|
+
...(processId === undefined || profileDir === undefined
|
|
631
|
+
? {}
|
|
632
|
+
: { identity: { processId, profileDir, targetUrl, ...(cdpPort === undefined ? {} : { cdpPort }) } }),
|
|
633
|
+
...(browserPreference === undefined
|
|
634
|
+
? {}
|
|
635
|
+
: { evidence: { requested: requestedBrowser, ...(resolved === undefined ? {} : { resolved }) } })
|
|
577
636
|
};
|
|
578
637
|
}
|
|
579
638
|
if (browserPreference === undefined || browserPreference === "default") {
|
|
@@ -583,20 +642,68 @@ async function openDesktopBrowserTarget(desktop, targetUrl, requestTimeoutMs, br
|
|
|
583
642
|
else {
|
|
584
643
|
await desktop.launch("google-chrome", targetUrl);
|
|
585
644
|
}
|
|
586
|
-
return
|
|
645
|
+
return {
|
|
646
|
+
family: desktop.open ? "unknown" : "chromium",
|
|
647
|
+
...(browserPreference === undefined ? {} : { evidence: { requested: requestedBrowser } })
|
|
648
|
+
};
|
|
587
649
|
}
|
|
588
650
|
const launchTarget = requestedBrowser === "chrome" ? "google-chrome"
|
|
589
651
|
: requestedBrowser === "chromium" ? "chromium"
|
|
590
652
|
: requestedBrowser === "firefox" ? "firefox"
|
|
591
653
|
: "google-chrome";
|
|
592
654
|
await desktop.launch(launchTarget, targetUrl);
|
|
593
|
-
return {
|
|
655
|
+
return {
|
|
656
|
+
family: desktopBrowserFamily(launchTarget),
|
|
657
|
+
evidence: { requested: requestedBrowser, resolved: launchTarget }
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
export function desktopBrowserFamily(value) {
|
|
661
|
+
if (value === "firefox")
|
|
662
|
+
return "firefox";
|
|
663
|
+
if (value === "chrome" || value === "chromium" || value === "google-chrome" || value === "google-chrome-stable" || value === "chromium-browser") {
|
|
664
|
+
return "chromium";
|
|
665
|
+
}
|
|
666
|
+
return "unknown";
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Observe-time CDP port resolution lines (pure; exported for contract tests): cached
|
|
670
|
+
* launch-time port first, then a re-read of the profile's DevToolsActivePort marker, then the
|
|
671
|
+
* legacy fixed 9222. The re-read is a local best-effort file read inside the already
|
|
672
|
+
* time-bounded observer command, so a missing/garbled marker degrades to the fallback,
|
|
673
|
+
* never a hang.
|
|
674
|
+
*/
|
|
675
|
+
export function chromeCdpPortResolutionScript(endpoint) {
|
|
676
|
+
return [
|
|
677
|
+
`let cdpPort = ${endpoint.cdpPort === undefined ? "undefined" : JSON.stringify(endpoint.cdpPort)};`,
|
|
678
|
+
`const cdpProfileDir = ${JSON.stringify(endpoint.profileDir ?? "")};`,
|
|
679
|
+
"if (cdpPort === undefined && cdpProfileDir) {",
|
|
680
|
+
" try {",
|
|
681
|
+
" const { readFileSync } = await import('node:fs');",
|
|
682
|
+
" const marker = readFileSync(cdpProfileDir + '/DevToolsActivePort', 'utf8');",
|
|
683
|
+
" const parsed = Number.parseInt(String(marker.split('\\n')[0] ?? ''), 10);",
|
|
684
|
+
" if (Number.isInteger(parsed) && parsed > 0) cdpPort = parsed;",
|
|
685
|
+
" } catch {}",
|
|
686
|
+
"}",
|
|
687
|
+
"if (cdpPort === undefined) cdpPort = 9222;"
|
|
688
|
+
];
|
|
594
689
|
}
|
|
595
|
-
|
|
690
|
+
/** Shared CDP page-selection preamble: pinned target id first, then this lane's target URL,
|
|
691
|
+
* then a single-page fallback; never an arbitrary page from a multi-page endpoint. */
|
|
692
|
+
function chromeCdpPageSelectionScript(endpoint, targetId) {
|
|
693
|
+
return [
|
|
694
|
+
...chromeCdpPortResolutionScript(endpoint),
|
|
695
|
+
"const pages = await fetch('http://127.0.0.1:' + cdpPort + '/json').then((r) => r.json()).catch(() => []);",
|
|
696
|
+
`const expectedTargetId = ${JSON.stringify(targetId ?? "")};`,
|
|
697
|
+
`const expectedTargetUrl = ${JSON.stringify(endpoint.targetUrl)};`,
|
|
698
|
+
"const normalizeUrl = (value) => String(value || '').replace(/\\/$/, '');",
|
|
699
|
+
"const httpPages = Array.isArray(pages) ? pages.filter((entry) => entry && entry.type === 'page' && /^https?:/.test(String(entry.url || ''))) : [];",
|
|
700
|
+
"const page = expectedTargetId ? httpPages.find((entry) => entry.id === expectedTargetId) : (httpPages.find((entry) => normalizeUrl(entry.url) === normalizeUrl(expectedTargetUrl)) || (httpPages.length === 1 ? httpPages[0] : undefined));"
|
|
701
|
+
];
|
|
702
|
+
}
|
|
703
|
+
export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoint, targetId) {
|
|
596
704
|
return async () => {
|
|
597
705
|
const script = [
|
|
598
|
-
|
|
599
|
-
"const page = Array.isArray(pages) ? pages.find((entry) => entry && entry.type === 'page' && /^https?:/.test(String(entry.url || ''))) : undefined;",
|
|
706
|
+
...chromeCdpPageSelectionScript(endpoint, targetId),
|
|
600
707
|
"if (!page) { console.log('{}'); process.exit(0); }",
|
|
601
708
|
"let text = '';",
|
|
602
709
|
"let url = String(page.url || '');",
|
|
@@ -648,6 +755,153 @@ export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs) {
|
|
|
648
755
|
}
|
|
649
756
|
};
|
|
650
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Read the running browser's actual outer-window bounds and CSS layout viewport through the
|
|
760
|
+
* already-enabled local Chrome DevTools endpoint. The returned values come from `window.*` in
|
|
761
|
+
* the target page; requested E2B resolution is deliberately not an input to this function.
|
|
762
|
+
*/
|
|
763
|
+
export function makeChromeDesktopGeometryObserver(desktop, requestTimeoutMs, endpoint, targetId) {
|
|
764
|
+
return async () => {
|
|
765
|
+
const script = [
|
|
766
|
+
...chromeCdpPageSelectionScript(endpoint, targetId),
|
|
767
|
+
"if (!page || typeof WebSocket !== 'function' || !page.webSocketDebuggerUrl) { console.log('{}'); process.exit(0); }",
|
|
768
|
+
"const ws = new WebSocket(page.webSocketDebuggerUrl);",
|
|
769
|
+
"const result = await new Promise((resolve) => {",
|
|
770
|
+
" const timer = setTimeout(() => resolve(undefined), 1500);",
|
|
771
|
+
" ws.onopen = () => ws.send(JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { returnByValue: true, expression: '({ browserWindow: { x: window.screenX, y: window.screenY, width: window.outerWidth, height: window.outerHeight }, viewport: { width: window.innerWidth, height: window.innerHeight, deviceScaleFactor: window.devicePixelRatio } })' } }));",
|
|
772
|
+
" ws.onmessage = (event) => {",
|
|
773
|
+
" try {",
|
|
774
|
+
" const payload = JSON.parse(String(event.data));",
|
|
775
|
+
" if (payload.id !== 1) return;",
|
|
776
|
+
" clearTimeout(timer);",
|
|
777
|
+
" resolve(payload.result && payload.result.result && payload.result.result.value);",
|
|
778
|
+
" } catch { clearTimeout(timer); resolve(undefined); }",
|
|
779
|
+
" };",
|
|
780
|
+
" ws.onerror = () => { clearTimeout(timer); resolve(undefined); };",
|
|
781
|
+
"}).finally(() => { try { ws.close(); } catch {} });",
|
|
782
|
+
"console.log(JSON.stringify(result ? { ...result, targetId: String(page.id || '') } : {}));"
|
|
783
|
+
].join("\n");
|
|
784
|
+
const result = await desktop.commands.run(`node --input-type=module -e ${shellSingleQuote(script)}`, {
|
|
785
|
+
requestTimeoutMs,
|
|
786
|
+
timeoutMs: 5_000
|
|
787
|
+
});
|
|
788
|
+
if (result.exitCode !== undefined && result.exitCode !== 0) {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
const parsed = JSON.parse((result.stdout ?? "{}").trim() || "{}");
|
|
793
|
+
if (!parsed || typeof parsed !== "object")
|
|
794
|
+
return undefined;
|
|
795
|
+
const record = parsed;
|
|
796
|
+
const rawWindow = record.browserWindow;
|
|
797
|
+
const rawViewport = record.viewport;
|
|
798
|
+
if (!isMeasuredRect(rawWindow) || !isMeasuredViewport(rawViewport))
|
|
799
|
+
return undefined;
|
|
800
|
+
return {
|
|
801
|
+
browserWindow: { ...rawWindow, source: "cdp" },
|
|
802
|
+
viewport: { ...rawViewport, source: "cdp" },
|
|
803
|
+
...(typeof record.targetId === "string" && record.targetId.length > 0 ? { targetId: record.targetId } : {})
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
return undefined;
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function isMeasuredRect(value) {
|
|
812
|
+
if (!value || typeof value !== "object")
|
|
813
|
+
return false;
|
|
814
|
+
const record = value;
|
|
815
|
+
return Number.isFinite(record.x)
|
|
816
|
+
&& Number.isFinite(record.y)
|
|
817
|
+
&& isPositiveMeasurement(record.width)
|
|
818
|
+
&& isPositiveMeasurement(record.height);
|
|
819
|
+
}
|
|
820
|
+
function isMeasuredViewport(value) {
|
|
821
|
+
if (!value || typeof value !== "object")
|
|
822
|
+
return false;
|
|
823
|
+
const record = value;
|
|
824
|
+
return isPositiveMeasurement(record.width)
|
|
825
|
+
&& isPositiveMeasurement(record.height)
|
|
826
|
+
&& isPositiveMeasurement(record.deviceScaleFactor);
|
|
827
|
+
}
|
|
828
|
+
function isPositiveMeasurement(value) {
|
|
829
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
830
|
+
}
|
|
831
|
+
async function measureBrowserWindowWithXdotool(desktop, windowId, requestTimeoutMs) {
|
|
832
|
+
const result = await desktop.commands.run([
|
|
833
|
+
"set -euo pipefail",
|
|
834
|
+
`win=${shellSingleQuote(windowId)}`,
|
|
835
|
+
"xdotool getwindowgeometry --shell \"$win\" 2>/dev/null || true"
|
|
836
|
+
].join("\n"), { requestTimeoutMs, timeoutMs: 5_000 });
|
|
837
|
+
const output = result.stdout ?? "";
|
|
838
|
+
const read = (name) => {
|
|
839
|
+
const raw = output.match(new RegExp(`^${name}=(-?\\d+)$`, "m"))?.[1];
|
|
840
|
+
if (raw === undefined)
|
|
841
|
+
return undefined;
|
|
842
|
+
const value = Number(raw);
|
|
843
|
+
return Number.isFinite(value) ? value : undefined;
|
|
844
|
+
};
|
|
845
|
+
const x = read("X");
|
|
846
|
+
const y = read("Y");
|
|
847
|
+
const width = read("WIDTH");
|
|
848
|
+
const height = read("HEIGHT");
|
|
849
|
+
if (x === undefined || y === undefined || width === undefined || height === undefined || width <= 0 || height <= 0) {
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
return { x, y, width, height, source: "xdotool" };
|
|
853
|
+
}
|
|
854
|
+
/** Shared hosted-browser geometry capture used by per-lane and sequential shared-world routes. */
|
|
855
|
+
export async function captureDesktopBrowserGeometry(args) {
|
|
856
|
+
const warnings = [];
|
|
857
|
+
let browserWindowId = args.browserWindowId;
|
|
858
|
+
if (browserWindowId === undefined && args.browserFamily !== "unknown") {
|
|
859
|
+
browserWindowId = await findVisibleBrowserWindowId(args.desktop, args.requestTimeoutMs, args.browserFamily, args.launchIdentity).catch((error) => {
|
|
860
|
+
warnings.push(`Browser window lookup failed for lane ${args.laneId}: ${redactText(toErrorMessage(error))}`);
|
|
861
|
+
return undefined;
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
let xdotoolWindow;
|
|
865
|
+
if (browserWindowId !== undefined) {
|
|
866
|
+
if (args.resize !== false) {
|
|
867
|
+
await fillDesktopBrowserWindow(args.desktop, browserWindowId, args.requestedScreen, args.requestTimeoutMs);
|
|
868
|
+
// Let the window manager apply the resize before querying both X and page layout geometry.
|
|
869
|
+
await args.desktop.wait(250).catch(() => undefined);
|
|
870
|
+
}
|
|
871
|
+
xdotoolWindow = await measureBrowserWindowWithXdotool(args.desktop, browserWindowId, args.requestTimeoutMs)
|
|
872
|
+
.catch(() => undefined);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
warnings.push(`Browser window bounds could not be measured for lane ${args.laneId}; the live stream will use the full desktop.`);
|
|
876
|
+
}
|
|
877
|
+
const chromeGeometry = args.browserFamily === "chromium"
|
|
878
|
+
? await makeChromeDesktopGeometryObserver(args.desktop, args.requestTimeoutMs, {
|
|
879
|
+
...(args.launchIdentity?.cdpPort === undefined ? {} : { cdpPort: args.launchIdentity.cdpPort }),
|
|
880
|
+
...(args.launchIdentity?.profileDir === undefined ? {} : { profileDir: args.launchIdentity.profileDir }),
|
|
881
|
+
targetUrl: args.targetUrl
|
|
882
|
+
}, args.browserTargetId)().catch(() => undefined)
|
|
883
|
+
: undefined;
|
|
884
|
+
const browserWindow = chromeGeometry?.browserWindow ?? xdotoolWindow;
|
|
885
|
+
const viewport = chromeGeometry?.viewport;
|
|
886
|
+
if (!browserWindow) {
|
|
887
|
+
warnings.push(`Browser outer bounds could not be measured for lane ${args.laneId}.`);
|
|
888
|
+
}
|
|
889
|
+
else if (browserWindow.width !== args.requestedScreen[0] || browserWindow.height !== args.requestedScreen[1]) {
|
|
890
|
+
warnings.push(`Browser window fill did not reach the requested ${args.requestedScreen[0]}x${args.requestedScreen[1]} screen for lane ${args.laneId}; measured outer bounds are ${browserWindow.width}x${browserWindow.height}.`);
|
|
891
|
+
}
|
|
892
|
+
if (!viewport) {
|
|
893
|
+
warnings.push(args.browserFamily === "firefox"
|
|
894
|
+
? `Browser CSS viewport measurement is unavailable for Firefox on lane ${args.laneId}; stream.viewport is omitted instead of reading a different browser's CDP endpoint.`
|
|
895
|
+
: `Browser CSS viewport could not be measured for lane ${args.laneId}; stream.viewport is omitted instead of copying the requested screen resolution.`);
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
...(browserWindowId === undefined ? {} : { browserWindowId }),
|
|
899
|
+
...(chromeGeometry?.targetId === undefined ? {} : { browserTargetId: chromeGeometry.targetId }),
|
|
900
|
+
...(browserWindow === undefined ? {} : { browserWindow }),
|
|
901
|
+
...(viewport === undefined ? {} : { viewport }),
|
|
902
|
+
warnings
|
|
903
|
+
};
|
|
904
|
+
}
|
|
651
905
|
function shellSingleQuote(value) {
|
|
652
906
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
653
907
|
}
|
|
@@ -714,6 +968,15 @@ export async function runCuaLane(spec, deps) {
|
|
|
714
968
|
let streamUrl;
|
|
715
969
|
let subjectCommit;
|
|
716
970
|
let desktopBrowser;
|
|
971
|
+
let launchedBrowserFamily = "unknown";
|
|
972
|
+
let browserLaunchIdentity;
|
|
973
|
+
let browserLaunched = false;
|
|
974
|
+
let initialBrowserGeometry;
|
|
975
|
+
let browserWindowId;
|
|
976
|
+
let browserTargetId;
|
|
977
|
+
let desktopGeometry = {
|
|
978
|
+
screen: { requested: { width: spec.resolution[0], height: spec.resolution[1] } }
|
|
979
|
+
};
|
|
717
980
|
let provisioned = false;
|
|
718
981
|
let signaled = false;
|
|
719
982
|
const signal = (ok) => {
|
|
@@ -754,12 +1017,37 @@ export async function runCuaLane(spec, deps) {
|
|
|
754
1017
|
await deps.hooks.prepareDesktop(desktop, { laneId: spec.laneId, laneIndex: spec.laneIndex, laneCount: deps.laneCount });
|
|
755
1018
|
}
|
|
756
1019
|
// Per-lane geometry assertion (fail-closed) — the device claim is verified in-sandbox.
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
1020
|
+
const screenGeometry = await inspectDesktopScreenGeometry({
|
|
1021
|
+
desktop,
|
|
1022
|
+
laneId: spec.laneId,
|
|
1023
|
+
requestedScreen: spec.resolution,
|
|
1024
|
+
requestTimeoutMs: deps.requestTimeoutMs
|
|
1025
|
+
});
|
|
1026
|
+
if (screenGeometry.verified) {
|
|
1027
|
+
desktopGeometry = {
|
|
1028
|
+
...desktopGeometry,
|
|
1029
|
+
screen: { ...desktopGeometry.screen, verified: screenGeometry.verified }
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
if (screenGeometry.warning) {
|
|
1033
|
+
warnings.push(screenGeometry.warning);
|
|
1034
|
+
desktopGeometry = { ...desktopGeometry, warnings: [screenGeometry.warning] };
|
|
1035
|
+
}
|
|
1036
|
+
if (screenGeometry.error && deps.screenMismatchPolicy !== "record-evidence") {
|
|
1037
|
+
sessionError = screenGeometry.error;
|
|
760
1038
|
failureCode = "HUMANISH_CUA_LAB_DEVICE_GEOMETRY";
|
|
761
1039
|
}
|
|
762
1040
|
else {
|
|
1041
|
+
if (screenGeometry.error && screenGeometry.verified) {
|
|
1042
|
+
// record-evidence policy: the bundle keeps requested vs verified as separate facts and
|
|
1043
|
+
// discloses the divergence instead of failing this lane's world mid-flight.
|
|
1044
|
+
const mismatchWarning = deps.scrubKnownValues(`Lane ${spec.laneId} requested a ${spec.resolution[0]}x${spec.resolution[1]} screen but xdpyinfo reports ${screenGeometry.verified.width}x${screenGeometry.verified.height}; recording requested vs verified separately instead of failing the lane closed.`);
|
|
1045
|
+
warnings.push(mismatchWarning);
|
|
1046
|
+
desktopGeometry = {
|
|
1047
|
+
...desktopGeometry,
|
|
1048
|
+
warnings: [...(desktopGeometry.warnings ?? []), mismatchWarning]
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
763
1051
|
if (cloneRoute && serve && subjectRepo) {
|
|
764
1052
|
subjectCommit = await provisionCloneSubject(desktop, {
|
|
765
1053
|
repo: subjectRepo,
|
|
@@ -793,20 +1081,28 @@ export async function runCuaLane(spec, deps) {
|
|
|
793
1081
|
...(deps.hooks.detachedTimers ?? {})
|
|
794
1082
|
});
|
|
795
1083
|
}
|
|
796
|
-
|
|
1084
|
+
const browserLaunch = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, config.execution?.desktop?.browser);
|
|
1085
|
+
desktopBrowser = browserLaunch.evidence;
|
|
1086
|
+
launchedBrowserFamily = browserLaunch.family;
|
|
1087
|
+
browserLaunchIdentity = browserLaunch.identity;
|
|
1088
|
+
browserLaunched = true;
|
|
797
1089
|
await desktop.wait(BROWSER_SETTLE_MS).catch(() => undefined);
|
|
798
1090
|
// World is ready: release the pipeline gate so the remaining lanes may start.
|
|
799
1091
|
provisioned = true;
|
|
800
1092
|
signal(true);
|
|
801
1093
|
try {
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1094
|
+
const browserGeometry = await captureDesktopBrowserGeometry({
|
|
1095
|
+
desktop,
|
|
1096
|
+
browserFamily: launchedBrowserFamily,
|
|
1097
|
+
...(browserLaunchIdentity === undefined ? {} : { launchIdentity: browserLaunchIdentity }),
|
|
1098
|
+
laneId: spec.laneId,
|
|
1099
|
+
targetUrl,
|
|
1100
|
+
requestedScreen: spec.resolution,
|
|
1101
|
+
requestTimeoutMs: deps.requestTimeoutMs
|
|
806
1102
|
});
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1103
|
+
initialBrowserGeometry = browserGeometry;
|
|
1104
|
+
browserWindowId = browserGeometry.browserWindowId;
|
|
1105
|
+
browserTargetId = browserGeometry.browserTargetId;
|
|
810
1106
|
await startDesktopStream(desktop, browserWindowId);
|
|
811
1107
|
const candidateStreamUrl = desktop.stream.getUrl({
|
|
812
1108
|
authKey: desktop.stream.getAuthKey(),
|
|
@@ -840,9 +1136,17 @@ export async function runCuaLane(spec, deps) {
|
|
|
840
1136
|
...(config.actors[0]?.model ? { model: config.actors[0].model } : {})
|
|
841
1137
|
},
|
|
842
1138
|
desktop: desktop,
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1139
|
+
...(launchedBrowserFamily === "chromium"
|
|
1140
|
+
? {
|
|
1141
|
+
executorOptions: {
|
|
1142
|
+
observeBrowserState: makeChromeBrowserStateObserver(desktop, deps.requestTimeoutMs, {
|
|
1143
|
+
...(browserLaunchIdentity?.cdpPort === undefined ? {} : { cdpPort: browserLaunchIdentity.cdpPort }),
|
|
1144
|
+
...(browserLaunchIdentity?.profileDir === undefined ? {} : { profileDir: browserLaunchIdentity.profileDir }),
|
|
1145
|
+
targetUrl
|
|
1146
|
+
}, browserTargetId)
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
: {}),
|
|
846
1150
|
redactScreenshots: deps.redactScreenshots,
|
|
847
1151
|
scrubText: deps.scrubKnownValues,
|
|
848
1152
|
writeScreenshot,
|
|
@@ -859,6 +1163,39 @@ export async function runCuaLane(spec, deps) {
|
|
|
859
1163
|
signal(false);
|
|
860
1164
|
}
|
|
861
1165
|
if (desktop && desktopModule) {
|
|
1166
|
+
if (browserLaunched) {
|
|
1167
|
+
const finalGeometry = await captureDesktopBrowserGeometry({
|
|
1168
|
+
desktop,
|
|
1169
|
+
browserFamily: launchedBrowserFamily,
|
|
1170
|
+
...(browserLaunchIdentity === undefined ? {} : { launchIdentity: browserLaunchIdentity }),
|
|
1171
|
+
...(browserWindowId === undefined ? {} : { browserWindowId }),
|
|
1172
|
+
...(browserTargetId === undefined ? {} : { browserTargetId }),
|
|
1173
|
+
laneId: spec.laneId,
|
|
1174
|
+
targetUrl,
|
|
1175
|
+
requestedScreen: spec.resolution,
|
|
1176
|
+
requestTimeoutMs: deps.requestTimeoutMs,
|
|
1177
|
+
resize: false
|
|
1178
|
+
}).catch((error) => ({
|
|
1179
|
+
warnings: [`Final browser geometry measurement failed for lane ${spec.laneId}: ${redactText(deps.scrubKnownValues(toErrorMessage(error)))}`]
|
|
1180
|
+
}));
|
|
1181
|
+
// Chosen capture rule: final-if-it-measured-anything, else launch-time. A final capture
|
|
1182
|
+
// that measured EITHER field wins whole, so a partial final capture omits fields the
|
|
1183
|
+
// launch-time capture had (honest omission); only a final capture that measured NOTHING
|
|
1184
|
+
// falls back to the launch-time capture.
|
|
1185
|
+
const chosenGeometry = finalGeometry.browserWindow !== undefined || finalGeometry.viewport !== undefined
|
|
1186
|
+
? finalGeometry
|
|
1187
|
+
: initialBrowserGeometry ?? finalGeometry;
|
|
1188
|
+
const geometryWarnings = [...new Set(chosenGeometry.warnings.map((warning) => deps.scrubKnownValues(warning)))];
|
|
1189
|
+
warnings.push(...geometryWarnings);
|
|
1190
|
+
desktopGeometry = {
|
|
1191
|
+
screen: desktopGeometry.screen,
|
|
1192
|
+
...(chosenGeometry.browserWindow === undefined ? {} : { browserWindow: chosenGeometry.browserWindow }),
|
|
1193
|
+
...(chosenGeometry.viewport === undefined ? {} : { viewport: chosenGeometry.viewport }),
|
|
1194
|
+
...((desktopGeometry.warnings?.length ?? 0) + geometryWarnings.length === 0
|
|
1195
|
+
? {}
|
|
1196
|
+
: { warnings: [...(desktopGeometry.warnings ?? []), ...geometryWarnings] })
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
862
1199
|
const failed = sessionError !== undefined || session === undefined;
|
|
863
1200
|
// Each route's own keep flag gates its own lane only: a clone.keep can never leak into
|
|
864
1201
|
// a local-tree lane's teardown decision, and vice versa.
|
|
@@ -917,6 +1254,7 @@ export async function runCuaLane(spec, deps) {
|
|
|
917
1254
|
screenshots,
|
|
918
1255
|
...(subjectCommit === undefined ? {} : { subjectCommit }),
|
|
919
1256
|
...(desktopBrowser === undefined ? {} : { desktopBrowser }),
|
|
1257
|
+
desktopGeometry,
|
|
920
1258
|
stateStepRecords,
|
|
921
1259
|
phaseRecords,
|
|
922
1260
|
warnings,
|
|
@@ -1769,7 +2107,8 @@ function buildSingleLaneBundle(args) {
|
|
|
1769
2107
|
mission: spec.instructions,
|
|
1770
2108
|
persona: spec.persona,
|
|
1771
2109
|
resolution: spec.resolution,
|
|
1772
|
-
|
|
2110
|
+
desktopRoute: !args.inProcessRoute,
|
|
2111
|
+
...(outcome?.desktopGeometry === undefined ? {} : { desktopGeometry: outcome.desktopGeometry }),
|
|
1773
2112
|
isMobile: spec.devicePreset.isMobile,
|
|
1774
2113
|
runId: args.runId,
|
|
1775
2114
|
screenshots: outcome?.screenshots ?? [],
|
|
@@ -2131,6 +2470,11 @@ export function buildCuaBundle(args) {
|
|
|
2131
2470
|
?? args.sessionError
|
|
2132
2471
|
?? "Contract bundle only: dry-run produced the evidence shape without launching a desktop or spending provider tokens.";
|
|
2133
2472
|
const lastScreenshot = args.screenshots[args.screenshots.length - 1];
|
|
2473
|
+
const desktopGeometry = args.desktopRoute === false
|
|
2474
|
+
? undefined
|
|
2475
|
+
: args.desktopGeometry ?? {
|
|
2476
|
+
screen: { requested: { width: args.resolution[0], height: args.resolution[1] } }
|
|
2477
|
+
};
|
|
2134
2478
|
// Honest labels (invariant 6: claims match mechanism): every screenshot label names the
|
|
2135
2479
|
// run's ACTUAL mode. The session trace is the evidence-of-record; the capture policy covers
|
|
2136
2480
|
// frames written before a mid-session failure produced a trace.
|
|
@@ -2174,12 +2518,17 @@ export function buildCuaBundle(args) {
|
|
|
2174
2518
|
embed: lastScreenshot
|
|
2175
2519
|
? { kind: "screenshot", url: lastScreenshot, title: `CUA desktop (${screenshotMode})` }
|
|
2176
2520
|
: { kind: "placeholder", title: "CUA desktop" },
|
|
2177
|
-
viewport
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2521
|
+
...(desktopGeometry?.viewport === undefined
|
|
2522
|
+
? {}
|
|
2523
|
+
: {
|
|
2524
|
+
viewport: {
|
|
2525
|
+
width: desktopGeometry.viewport.width,
|
|
2526
|
+
height: desktopGeometry.viewport.height,
|
|
2527
|
+
deviceScaleFactor: desktopGeometry.viewport.deviceScaleFactor,
|
|
2528
|
+
...(args.isMobile === undefined ? {} : { isMobile: args.isMobile })
|
|
2529
|
+
}
|
|
2530
|
+
}),
|
|
2531
|
+
...(desktopGeometry === undefined ? {} : { desktopGeometry }),
|
|
2183
2532
|
ui: {
|
|
2184
2533
|
route: publicAppUrl,
|
|
2185
2534
|
intent: "Watch the computer-use actor drive the subject app in a hosted desktop browser.",
|
|
@@ -2293,6 +2642,17 @@ export function buildCuaBundle(args) {
|
|
|
2293
2642
|
streamId: "stream-001"
|
|
2294
2643
|
});
|
|
2295
2644
|
}
|
|
2645
|
+
for (const warning of desktopGeometry?.warnings ?? []) {
|
|
2646
|
+
events.push({
|
|
2647
|
+
id: `event-${String(phaseEventSeq++).padStart(3, "0")}-geometry-warning`,
|
|
2648
|
+
at: args.createdAt,
|
|
2649
|
+
level: "warn",
|
|
2650
|
+
type: "cua-lab.geometry.warning",
|
|
2651
|
+
message: warning,
|
|
2652
|
+
simId: "sim-001",
|
|
2653
|
+
streamId: "stream-001"
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2296
2656
|
const review = {
|
|
2297
2657
|
schema: REVIEW_SCHEMA,
|
|
2298
2658
|
verdict: args.inProgress === true
|
|
@@ -2439,6 +2799,9 @@ export function buildCuaFanoutBundle(args) {
|
|
|
2439
2799
|
const publicLaneAppUrl = publicSafeAppUrlLabel(laneAppUrl);
|
|
2440
2800
|
const subject = args.laneSubjects[index];
|
|
2441
2801
|
const session = outcome?.session;
|
|
2802
|
+
const desktopGeometry = outcome?.desktopGeometry ?? {
|
|
2803
|
+
screen: { requested: { width: spec.resolution[0], height: spec.resolution[1] } }
|
|
2804
|
+
};
|
|
2442
2805
|
const screenshots = outcome?.screenshots ?? [];
|
|
2443
2806
|
const lastScreenshot = screenshots[screenshots.length - 1];
|
|
2444
2807
|
const status = args.inProgress === true && outcome === undefined
|
|
@@ -2498,12 +2861,17 @@ export function buildCuaFanoutBundle(args) {
|
|
|
2498
2861
|
embed: lastScreenshot
|
|
2499
2862
|
? { kind: "screenshot", url: lastScreenshot, title: `CUA desktop ${spec.laneId} (${screenshotMode})` }
|
|
2500
2863
|
: { kind: "placeholder", title: `CUA desktop ${spec.laneId}` },
|
|
2501
|
-
viewport
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2864
|
+
...(desktopGeometry.viewport === undefined
|
|
2865
|
+
? {}
|
|
2866
|
+
: {
|
|
2867
|
+
viewport: {
|
|
2868
|
+
width: desktopGeometry.viewport.width,
|
|
2869
|
+
height: desktopGeometry.viewport.height,
|
|
2870
|
+
deviceScaleFactor: desktopGeometry.viewport.deviceScaleFactor,
|
|
2871
|
+
isMobile: spec.devicePreset.isMobile
|
|
2872
|
+
}
|
|
2873
|
+
}),
|
|
2874
|
+
desktopGeometry,
|
|
2507
2875
|
ui: {
|
|
2508
2876
|
route: publicLaneAppUrl,
|
|
2509
2877
|
intent: `Watch lane ${spec.laneId} (${spec.persona.id}/${spec.deviceName}) drive the subject app in its own hosted desktop.`,
|
|
@@ -2628,6 +2996,17 @@ export function buildCuaFanoutBundle(args) {
|
|
|
2628
2996
|
streamId: spec.streamId
|
|
2629
2997
|
});
|
|
2630
2998
|
}
|
|
2999
|
+
for (const warning of desktopGeometry.warnings ?? []) {
|
|
3000
|
+
events.push({
|
|
3001
|
+
id: nextEventId(`geometry-warning-${spec.laneId}`),
|
|
3002
|
+
at: args.createdAt,
|
|
3003
|
+
level: "warn",
|
|
3004
|
+
type: "cua-lab.geometry.warning",
|
|
3005
|
+
message: warning,
|
|
3006
|
+
simId: spec.simId,
|
|
3007
|
+
streamId: spec.streamId
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
2631
3010
|
// Persisted per-lane phase trail (real boot timing): one RunEvent per COMPLETED phase
|
|
2632
3011
|
// boundary this lane recorded (started events never persist here; they carry no durationMs).
|
|
2633
3012
|
for (const phase of outcome?.phaseRecords ?? []) {
|