humanish 0.15.3 → 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.
@@ -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/viewport comes from the named-preset registry (device-presets.ts), selectable per run
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 assumed).
407
- * Returns a fail-closed DEVICE_GEOMETRY message on a PARSEABLE mismatch. When xdpyinfo is
408
- * unavailable or unparseable (only in fakes a real E2B desktop always answers), it cannot be
409
- * verified, so the lane proceeds with the requested geometry (no false failure).
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 checkLaneGeometry(desktop, spec, requestTimeoutMs) {
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 undefined;
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 undefined;
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] = spec.resolution;
426
+ const [expectedWidth, expectedHeight] = args.requestedScreen;
427
427
  if (width === expectedWidth && height === expectedHeight) {
428
- return undefined;
428
+ return { verified: { width, height, source: "xdpyinfo" } };
429
429
  }
430
- return `HUMANISH_CUA_LAB_DEVICE_GEOMETRY: lane ${spec.laneId} requested a ${expectedWidth}x${expectedHeight} desktop but xdpyinfo reports ${width}x${height} in-sandbox; the per-lane device geometry is unverified (fail-closed).`;
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
- "find_chrome_window() {",
453
- " timeout 2s xdotool search --onlyvisible --class 'google-chrome|Google-chrome|chromium|Chromium|chrome|Chrome' 2>/dev/null | tail -n 1 || true",
454
- "}",
455
- "find_firefox_window() {",
456
- " timeout 2s xdotool search --onlyvisible --class 'firefox|Firefox' 2>/dev/null | tail -n 1 || true",
457
- "}",
458
- "find_named_window() {",
459
- " timeout 2s xdotool search --onlyvisible --name 'Google Chrome|Chromium|Mozilla Firefox|127[.]0[.]0[.]1|localhost|e2b[.]app|e2b[.]dev' 2>/dev/null | tail -n 1 || true",
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
- "window_id=",
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=/tmp/humanish-chrome-profile",
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=9222 "--user-data-dir=$chrome_profile_dir" ${chromiumFlags})`,
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
- " launch_browser google-chrome google-chrome --new-window \"${chrome_debug_flags[@]}\" && return 0",
537
- " launch_browser google-chrome-stable google-chrome-stable --new-window \"${chrome_debug_flags[@]}\" && return 0",
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
- " launch_browser chromium chromium --new-window \"${chrome_debug_flags[@]}\" && return 0",
543
- " launch_browser chromium-browser chromium-browser --new-window \"${chrome_debug_flags[@]}\" && return 0",
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
- " launch_browser firefox firefox --new-window && return 0",
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
- " launch_browser google-chrome google-chrome --new-window \"${chrome_debug_flags[@]}\" && return 0",
554
- " launch_browser google-chrome-stable google-chrome-stable --new-window \"${chrome_debug_flags[@]}\" && return 0",
555
- " launch_browser chromium chromium --new-window \"${chrome_debug_flags[@]}\" && return 0",
556
- " launch_browser chromium-browser chromium-browser --new-window \"${chrome_debug_flags[@]}\" && return 0",
557
- " launch_browser firefox firefox --new-window && return 0",
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
- return browserPreference === undefined ? undefined : {
575
- requested: requestedBrowser,
576
- ...(resolved === undefined ? {} : { resolved })
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 browserPreference === undefined ? undefined : { requested: requestedBrowser };
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 { requested: requestedBrowser, resolved: launchTarget };
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
- export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs) {
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
- "const pages = await fetch('http://127.0.0.1:9222/json').then((r) => r.json()).catch(() => []);",
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 geometryError = await checkLaneGeometry(desktop, spec, deps.requestTimeoutMs);
758
- if (geometryError) {
759
- sessionError = geometryError;
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
- desktopBrowser = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, config.execution?.desktop?.browser);
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 browserWindowId = await findVisibleBrowserWindowId(desktop, deps.requestTimeoutMs)
803
- .catch((error) => {
804
- warnings.push(`Browser window lookup failed before live stream start (falling back to desktop stream): ${redactText(deps.scrubKnownValues(toErrorMessage(error)))}`);
805
- return undefined;
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
- if (browserWindowId !== undefined) {
808
- await fillDesktopBrowserWindow(desktop, browserWindowId, spec.resolution, deps.requestTimeoutMs);
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
- executorOptions: {
844
- observeBrowserState: makeChromeBrowserStateObserver(desktop, deps.requestTimeoutMs)
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
- deviceScaleFactor: spec.devicePreset.deviceScaleFactor,
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
- width: args.resolution[0],
2179
- height: args.resolution[1],
2180
- deviceScaleFactor: args.deviceScaleFactor ?? 1,
2181
- ...(args.isMobile === undefined ? {} : { isMobile: args.isMobile })
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
- width: spec.resolution[0],
2503
- height: spec.resolution[1],
2504
- deviceScaleFactor: spec.devicePreset.deviceScaleFactor,
2505
- isMobile: spec.devicePreset.isMobile
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 ?? []) {