humanish 0.74.0 → 0.76.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 +28 -11
- package/dist/chrome-cdp-probe.d.ts +93 -0
- package/dist/chrome-cdp-probe.js +395 -0
- package/dist/chrome-cdp-probe.js.map +1 -0
- package/dist/cua-actor-lab.d.ts +29 -10
- package/dist/cua-actor-lab.js +251 -154
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/feedback.d.ts +20 -5
- package/dist/feedback.js +47 -16
- package/dist/feedback.js.map +1 -1
- package/dist/lab-config.d.ts +21 -0
- package/dist/lab-config.js +30 -0
- package/dist/lab-config.js.map +1 -1
- package/dist/pricing.js +15 -6
- package/dist/pricing.js.map +1 -1
- package/dist/program.js +26 -8
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +26 -0
- package/dist/run.js.map +1 -1
- package/dist/telemetry.d.ts +10 -0
- package/dist/telemetry.js +12 -0
- package/dist/telemetry.js.map +1 -1
- package/docs/contracts/schemas.md +4 -2
- package/docs/goals/current.md +25 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,12 +53,14 @@ npx humanish run first-run # a study with no keys and no spend — about a mi
|
|
|
53
53
|
npx humanish run try-live # a REAL study on a hosted desktop, capped at $2
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
**What it finds, measured (2026-09-01, receipts in `bench/` and `docs/goals/`):**
|
|
57
|
-
defects over
|
|
58
|
-
did not write, 16 of 18 distinct findings confirmed against the source and 0 invented,
|
|
59
|
-
participants;
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
**What it finds, measured (2026-09-01 and 09-03, receipts in `bench/` and `docs/goals/`):** 43 of
|
|
57
|
+
45 planted defects over three benchmark runs on an app we wrote, none invented on the clean arm; on
|
|
58
|
+
two apps we did not write, 16 of 18 distinct findings confirmed against the source and 0 invented,
|
|
59
|
+
across 14 participants; a drawDB modal reported by 5 of 5 keyboard-first participants (3 stopped
|
|
60
|
+
there) and never mentioned by 5 mouse-driving newcomers; a TodoMVC rename that blocked 6 of 6
|
|
61
|
+
keyboard-first participants and none of the others; and, on a phone-sized desktop, a drawDB
|
|
62
|
+
relationship drag intercepted by a popover that both phone participants hit (one stopped) and no
|
|
63
|
+
desktop participant reported. Each file states its own caveats.
|
|
62
64
|
|
|
63
65
|
`try-live` studies a real public app so that your first live run works without
|
|
64
66
|
configuring anything; point its `subject` at your own app once you have seen it
|
|
@@ -343,7 +345,8 @@ persisted evidence: screenshots, events, terminal tails). See
|
|
|
343
345
|
| `humanish runs` | List local runs and latest pointers. |
|
|
344
346
|
| `humanish export` | One self-contained `.html` of a run's Observer with screenshots inlined; verify and the share_ready gate run inside, `--local-only` watermarks a raw-screenshot bundle. |
|
|
345
347
|
| `humanish stats` | Cost, outcome, and duration roll-ups across run history; `--lab`, `--since`. Estimates stay labelled; unknown costs count as unknown. |
|
|
346
|
-
| `humanish feedback
|
|
348
|
+
| `humanish feedback list` | List a run's draft state and every feedback candidate (one per participant finding), with the ids `--candidate` takes. |
|
|
349
|
+
| `humanish feedback issue` | Print a public-safe GitHub issue draft without API mutation. `--candidate <id>` chooses which finding; default is the first. |
|
|
347
350
|
| `humanish lab run oss` | Repo-maintainer contract example: dry-run Observer-of-Observers for authorized repo selections. |
|
|
348
351
|
| `humanish lab run oss-smoke` | Repo-maintainer dogfood example: disposable clone smoke test against public OSS repos. |
|
|
349
352
|
|
|
@@ -463,12 +466,26 @@ viewport is smaller because browser chrome occupies space; Chromium-family live
|
|
|
463
466
|
measure it through CDP and record it separately from requested/verified screen geometry.
|
|
464
467
|
Browsers without that measurement seam omit the viewport rather than guessing. A
|
|
465
468
|
site's width-based responsive CSS still fires, and the model is *told* its device in the
|
|
466
|
-
prompt, matching how those sims run organic mobile lanes.
|
|
467
|
-
device-pixel-ratio isn't rendered, and the user-agent stays desktop on this
|
|
468
|
-
|
|
469
|
-
today; per-*persona* device (N personas × devices) lands with fan-out.
|
|
469
|
+
prompt, matching how those sims run organic mobile lanes. Without the block below there is no
|
|
470
|
+
touch input, the device-pixel-ratio isn't rendered, and the user-agent stays desktop on this
|
|
471
|
+
route. Device is run-wide today; per-*persona* device (N personas × devices) lands with fan-out.
|
|
470
472
|
`execution.desktop.resolution` is a raw escape hatch that overrides the preset.
|
|
471
473
|
|
|
474
|
+
**Mobile emulation.** `execution.desktop.fidelity: { mobileEmulation: true }` turns every hosted
|
|
475
|
+
Chrome/Chromium computer-use lane on a mobile preset (`mobile`, `small-mobile`, `narrow-mobile`)
|
|
476
|
+
into a mobile-emulated browser before the participant arrives, and leaves desktop, tablet and
|
|
477
|
+
wide lanes in the same run untouched: the lane's preset width/height become the CSS viewport (414 px for `mobile`, where the
|
|
478
|
+
X screen itself cannot go below 500), the preset's device pixel ratio applies (`deviceScaleFactor`
|
|
479
|
+
overrides it), touch events are on (`touch: false` turns them off) and the browser presents a
|
|
480
|
+
mobile user agent (`userAgent` replaces the default iPhone Safari string). The run bundle records
|
|
481
|
+
`desktopGeometry.fidelity` with `tier: mobile-emulated`, the request, the CDP methods applied,
|
|
482
|
+
and `resolved`: what the page itself reported afterwards (`navigator.userAgent`,
|
|
483
|
+
`devicePixelRatio`, `innerWidth`, `maxTouchPoints`, coarse pointer). A page without a viewport
|
|
484
|
+
meta lays out at 980 px, as it would on a phone, and the bundle says so. Firefox cannot be
|
|
485
|
+
emulated, so the lane fails closed instead of shipping a desktop run labelled mobile. The
|
|
486
|
+
emulation covers the launch tab (the user agent and touch flags are browser-wide); a bundle
|
|
487
|
+
without a `fidelity` block is a responsive-viewport study whatever its preset is called.
|
|
488
|
+
|
|
472
489
|
**Desktop browser choice.** Hosted computer-use lanes and shared-world actor seats use the
|
|
473
490
|
route's historical opener unless you set `execution.desktop.browser` to `chrome`, `chromium`,
|
|
474
491
|
or `firefox`. A concrete value means "launch this browser or fail"; it never silently
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Which target to attribute when the endpoint lists several pages. */
|
|
2
|
+
export type ChromeCdpPagePreference = "pinned" | "active";
|
|
3
|
+
export interface ChromeCdpProbeArgs {
|
|
4
|
+
/** Launch-time CDP port, if the launch capture caught it. */
|
|
5
|
+
cdpPort?: number;
|
|
6
|
+
/** The launched profile dir; the probe re-reads DevToolsActivePort at observe time. */
|
|
7
|
+
profileDir?: string;
|
|
8
|
+
/** The URL this lane opened; attributes the page when no target id is pinned yet. */
|
|
9
|
+
targetUrl: string;
|
|
10
|
+
/** The pinned page target id from the launch-time geometry capture. */
|
|
11
|
+
targetId?: string;
|
|
12
|
+
/**
|
|
13
|
+
* "pinned" (default): the launch-time target, for measurements about the ORIGINAL window
|
|
14
|
+
* (geometry). "active": the tab the participant is driving NOW — Chrome's /json lists page
|
|
15
|
+
* targets most-recently-focused first. The state observer must follow the participant: a
|
|
16
|
+
* verification link that opens in a NEW tab left a pinned observer reading the old tab forever,
|
|
17
|
+
* so the observed URL never changed again and stopWhen/task criteria went blind (a live run's
|
|
18
|
+
* funnel read reach-dashboard 0/2 under a screenshot OF the dashboard).
|
|
19
|
+
*/
|
|
20
|
+
prefer?: ChromeCdpPagePreference;
|
|
21
|
+
/**
|
|
22
|
+
* "state": url/title/text/scrollY. "geometry": outer window + CSS viewport. "port": resolution
|
|
23
|
+
* only. "emulate": apply mobile emulation (#221) to the selected page and exit (the overrides that
|
|
24
|
+
* are session-scoped, UA / touch / DPR, lapse when the socket closes). "hold": the same, then keep
|
|
25
|
+
* the socket open until killed, which is how a lane keeps them for its whole life. "fidelity": read
|
|
26
|
+
* back what the page reports about itself (UA, DPR, viewport, touch), the proof for the bundle.
|
|
27
|
+
*/
|
|
28
|
+
mode: "state" | "geometry" | "port" | "emulate" | "hold" | "fidelity";
|
|
29
|
+
/** For "emulate": what to apply. */
|
|
30
|
+
emulation?: ChromeMobileEmulationRequest;
|
|
31
|
+
}
|
|
32
|
+
/** Mobile emulation request (#221): the CDP Emulation domain applied to one page target. */
|
|
33
|
+
export interface ChromeMobileEmulationRequest {
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
deviceScaleFactor: number;
|
|
37
|
+
touch: boolean;
|
|
38
|
+
userAgent: string;
|
|
39
|
+
platform?: string;
|
|
40
|
+
}
|
|
41
|
+
/** What the page reports after emulation: the proof, never copied from the request. */
|
|
42
|
+
export interface ChromeFidelityRead {
|
|
43
|
+
userAgent: string;
|
|
44
|
+
devicePixelRatio: number;
|
|
45
|
+
innerWidth: number;
|
|
46
|
+
innerHeight: number;
|
|
47
|
+
maxTouchPoints: number;
|
|
48
|
+
coarsePointer: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** The probe's stdout, before the caller narrows it. */
|
|
51
|
+
export interface ChromeCdpProbeResult {
|
|
52
|
+
unavailable?: string;
|
|
53
|
+
cdpPort?: number;
|
|
54
|
+
url?: string;
|
|
55
|
+
title?: string;
|
|
56
|
+
text?: string;
|
|
57
|
+
scrollY?: number;
|
|
58
|
+
targetId?: string;
|
|
59
|
+
browserWindow?: {
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
};
|
|
65
|
+
viewport?: {
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
deviceScaleFactor: number;
|
|
69
|
+
};
|
|
70
|
+
/** "emulate": the CDP methods that returned without error, in order. */
|
|
71
|
+
applied?: string[];
|
|
72
|
+
/** "fidelity": the read-back. */
|
|
73
|
+
fidelity?: ChromeFidelityRead;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The probe itself. Kept as one string so the shipped command is exactly what the tests execute
|
|
77
|
+
* (tests/chrome-cdp-probe.test.ts runs it under the real python3 against a real headless Chrome).
|
|
78
|
+
*
|
|
79
|
+
* WebSocket is hand-rolled because python's stdlib has no client: one masked text frame out, frames
|
|
80
|
+
* in until the reply with id 1 arrives, 1.5 s budget, and NO Origin header (Chrome refuses
|
|
81
|
+
* cross-origin DevTools sockets unless --remote-allow-origins is set; a header-less client is a
|
|
82
|
+
* local one). urllib is opened WITHOUT proxy handlers so a sandbox-wide http_proxy cannot redirect
|
|
83
|
+
* a loopback read.
|
|
84
|
+
*/
|
|
85
|
+
export declare const CHROME_CDP_PROBE_PY: string;
|
|
86
|
+
/** The exact shell command a sandbox runs for one probe. */
|
|
87
|
+
export declare function chromeCdpProbeCommand(args: ChromeCdpProbeArgs): string;
|
|
88
|
+
/**
|
|
89
|
+
* Narrow one probe's stdout. A parse failure is reported as unavailable with the reason, never as
|
|
90
|
+
* an empty success: the difference between "nothing to observe" and "could not observe" is the
|
|
91
|
+
* whole point of #514.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseChromeCdpProbeOutput(stdout: string | undefined): ChromeCdpProbeResult;
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// The in-sandbox Chrome DevTools probe behind every URL / page-text / viewport observation.
|
|
2
|
+
//
|
|
3
|
+
// It runs on python3, stdlib only. It used to run on node, and that was the #514 root cause: the
|
|
4
|
+
// stock E2B desktop template ships python3 and curl but NO Node, and Node only arrives when a
|
|
5
|
+
// subject's serve pipeline needs it (subject-runtime.ts). So on the app-url route, and on any
|
|
6
|
+
// subject served by something other than Node (the taskly benchmark is `python3 -m http.server`),
|
|
7
|
+
// `node -e` exited 127 on every turn, the probe degraded to `{}`, and every urlIncludes /
|
|
8
|
+
// textIncludes stop condition and task criterion went blind for the whole session. The only trace
|
|
9
|
+
// was a geometry warning that the CSS viewport "could not be measured", which named the symptom
|
|
10
|
+
// and not the cause. The tab-pinning fix that preceded this one (prefer "active") was diagnosed on
|
|
11
|
+
// a Node subject, where the probe happened to work.
|
|
12
|
+
//
|
|
13
|
+
// The same lesson was learned once already: the comms catch was rewritten from node to python3 in
|
|
14
|
+
// 0.29.0 (comms-sandbox-catch.ts). This is the third in-sandbox runtime dependency to move.
|
|
15
|
+
//
|
|
16
|
+
// The script takes ONE JSON argument and prints ONE JSON line. Failures print
|
|
17
|
+
// `{"unavailable": "<reason>"}` with exit 0 so the caller can say WHY the channel is dark instead
|
|
18
|
+
// of swallowing an exit code; the TypeScript side turns that into a lane warning that names the
|
|
19
|
+
// consequence ("url/text criteria will read as NEVER MEASURED").
|
|
20
|
+
/**
|
|
21
|
+
* The probe itself. Kept as one string so the shipped command is exactly what the tests execute
|
|
22
|
+
* (tests/chrome-cdp-probe.test.ts runs it under the real python3 against a real headless Chrome).
|
|
23
|
+
*
|
|
24
|
+
* WebSocket is hand-rolled because python's stdlib has no client: one masked text frame out, frames
|
|
25
|
+
* in until the reply with id 1 arrives, 1.5 s budget, and NO Origin header (Chrome refuses
|
|
26
|
+
* cross-origin DevTools sockets unless --remote-allow-origins is set; a header-less client is a
|
|
27
|
+
* local one). urllib is opened WITHOUT proxy handlers so a sandbox-wide http_proxy cannot redirect
|
|
28
|
+
* a loopback read.
|
|
29
|
+
*/
|
|
30
|
+
export const CHROME_CDP_PROBE_PY = String.raw `
|
|
31
|
+
import base64, json, os, re, socket, struct, sys, time, urllib.request
|
|
32
|
+
|
|
33
|
+
def resolve_port(args):
|
|
34
|
+
port = args.get("cdpPort")
|
|
35
|
+
if isinstance(port, int) and port > 0:
|
|
36
|
+
return port
|
|
37
|
+
profile_dir = str(args.get("profileDir") or "")
|
|
38
|
+
if profile_dir:
|
|
39
|
+
try:
|
|
40
|
+
with open(os.path.join(profile_dir, "DevToolsActivePort"), "r", encoding="utf-8") as handle:
|
|
41
|
+
first = handle.readline().strip()
|
|
42
|
+
parsed = int(first)
|
|
43
|
+
if parsed > 0:
|
|
44
|
+
return parsed
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
return 9222
|
|
48
|
+
|
|
49
|
+
def list_pages(port):
|
|
50
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
51
|
+
with opener.open("http://127.0.0.1:%d/json" % port, timeout=2) as response:
|
|
52
|
+
pages = json.loads(response.read().decode("utf-8"))
|
|
53
|
+
return pages if isinstance(pages, list) else []
|
|
54
|
+
|
|
55
|
+
def select_page(pages, args):
|
|
56
|
+
http_pages = [
|
|
57
|
+
page for page in pages
|
|
58
|
+
if isinstance(page, dict) and page.get("type") == "page" and re.match(r"^https?:", str(page.get("url") or ""))
|
|
59
|
+
]
|
|
60
|
+
target_id = str(args.get("targetId") or "")
|
|
61
|
+
target_url = str(args.get("targetUrl") or "")
|
|
62
|
+
normalize = lambda value: str(value or "").rstrip("/")
|
|
63
|
+
if args.get("prefer") == "active":
|
|
64
|
+
if http_pages:
|
|
65
|
+
return http_pages[0]
|
|
66
|
+
if target_id:
|
|
67
|
+
return next((page for page in http_pages if page.get("id") == target_id), None)
|
|
68
|
+
return None
|
|
69
|
+
if target_id:
|
|
70
|
+
return next((page for page in http_pages if page.get("id") == target_id), None)
|
|
71
|
+
match = next((page for page in http_pages if normalize(page.get("url")) == normalize(target_url)), None)
|
|
72
|
+
if match is not None:
|
|
73
|
+
return match
|
|
74
|
+
return http_pages[0] if len(http_pages) == 1 else None
|
|
75
|
+
|
|
76
|
+
def ws_session(ws_url, messages, timeout=1.5, hold=False):
|
|
77
|
+
"""Send CDP messages over one page socket, in order, and return their replies (None on failure).
|
|
78
|
+
hold=True prints the replies and then keeps the socket open until the process is killed."""
|
|
79
|
+
match = re.match(r"^ws://([^/:]+):(\d+)(/.*)$", str(ws_url or ""))
|
|
80
|
+
if not match:
|
|
81
|
+
return None
|
|
82
|
+
host, port, path = match.group(1), int(match.group(2)), match.group(3)
|
|
83
|
+
sock = None
|
|
84
|
+
try:
|
|
85
|
+
sock = socket.create_connection((host, port), timeout=timeout)
|
|
86
|
+
sock.settimeout(timeout)
|
|
87
|
+
key = base64.b64encode(os.urandom(16)).decode("ascii")
|
|
88
|
+
handshake = (
|
|
89
|
+
"GET %s HTTP/1.1\r\nHost: %s:%d\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
|
90
|
+
"Sec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n\r\n" % (path, host, port, key)
|
|
91
|
+
)
|
|
92
|
+
sock.sendall(handshake.encode("ascii"))
|
|
93
|
+
buffer = b""
|
|
94
|
+
while b"\r\n\r\n" not in buffer:
|
|
95
|
+
chunk = sock.recv(4096)
|
|
96
|
+
if not chunk:
|
|
97
|
+
return None
|
|
98
|
+
buffer += chunk
|
|
99
|
+
head, buffer = buffer.split(b"\r\n\r\n", 1)
|
|
100
|
+
if b" 101 " not in head.split(b"\r\n", 1)[0]:
|
|
101
|
+
return None
|
|
102
|
+
state = {"buffer": buffer}
|
|
103
|
+
|
|
104
|
+
def need(count):
|
|
105
|
+
while len(state["buffer"]) < count:
|
|
106
|
+
chunk = sock.recv(65536)
|
|
107
|
+
if not chunk:
|
|
108
|
+
raise EOFError("socket closed")
|
|
109
|
+
state["buffer"] += chunk
|
|
110
|
+
|
|
111
|
+
def send(message):
|
|
112
|
+
payload = json.dumps(message).encode("utf-8")
|
|
113
|
+
mask = os.urandom(4)
|
|
114
|
+
frame = bytearray([0x81])
|
|
115
|
+
size = len(payload)
|
|
116
|
+
if size < 126:
|
|
117
|
+
frame.append(0x80 | size)
|
|
118
|
+
elif size < 65536:
|
|
119
|
+
frame.append(0x80 | 126)
|
|
120
|
+
frame += struct.pack(">H", size)
|
|
121
|
+
else:
|
|
122
|
+
frame.append(0x80 | 127)
|
|
123
|
+
frame += struct.pack(">Q", size)
|
|
124
|
+
frame += mask + bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload))
|
|
125
|
+
sock.sendall(bytes(frame))
|
|
126
|
+
|
|
127
|
+
def receive(wanted_id):
|
|
128
|
+
message = b""
|
|
129
|
+
while True:
|
|
130
|
+
need(2)
|
|
131
|
+
first, second = state["buffer"][0], state["buffer"][1]
|
|
132
|
+
fin, opcode = first & 0x80, first & 0x0F
|
|
133
|
+
masked, length, offset = second & 0x80, second & 0x7F, 2
|
|
134
|
+
if length == 126:
|
|
135
|
+
need(4)
|
|
136
|
+
length, offset = struct.unpack(">H", state["buffer"][2:4])[0], 4
|
|
137
|
+
elif length == 127:
|
|
138
|
+
need(10)
|
|
139
|
+
length, offset = struct.unpack(">Q", state["buffer"][2:10])[0], 10
|
|
140
|
+
frame_mask = b""
|
|
141
|
+
if masked:
|
|
142
|
+
need(offset + 4)
|
|
143
|
+
frame_mask, offset = state["buffer"][offset:offset + 4], offset + 4
|
|
144
|
+
need(offset + length)
|
|
145
|
+
data = state["buffer"][offset:offset + length]
|
|
146
|
+
state["buffer"] = state["buffer"][offset + length:]
|
|
147
|
+
if masked:
|
|
148
|
+
data = bytes(byte ^ frame_mask[index % 4] for index, byte in enumerate(data))
|
|
149
|
+
if opcode == 8:
|
|
150
|
+
return None
|
|
151
|
+
if opcode in (9, 10):
|
|
152
|
+
continue
|
|
153
|
+
message += data
|
|
154
|
+
if fin:
|
|
155
|
+
try:
|
|
156
|
+
reply = json.loads(message.decode("utf-8"))
|
|
157
|
+
except Exception:
|
|
158
|
+
return None
|
|
159
|
+
message = b""
|
|
160
|
+
if isinstance(reply, dict) and reply.get("id") == wanted_id:
|
|
161
|
+
return reply
|
|
162
|
+
|
|
163
|
+
replies = []
|
|
164
|
+
for index, message in enumerate(messages, 1):
|
|
165
|
+
send({"id": index, **message})
|
|
166
|
+
replies.append(receive(index))
|
|
167
|
+
if hold:
|
|
168
|
+
# Announce the result now (the caller reads stdout once), then stay attached.
|
|
169
|
+
applied = [m["method"] for m, r in zip(messages, replies) if isinstance(r, dict) and "error" not in r]
|
|
170
|
+
failed = [m["method"] for m, r in zip(messages, replies) if not (isinstance(r, dict) and "error" not in r)]
|
|
171
|
+
print(json.dumps({"applied": applied, "held": not failed, **({"unavailable": "%s failed" % failed[0]} if failed else {})}), flush=True)
|
|
172
|
+
if failed:
|
|
173
|
+
return replies
|
|
174
|
+
sock.settimeout(None)
|
|
175
|
+
while True:
|
|
176
|
+
time.sleep(30)
|
|
177
|
+
return replies
|
|
178
|
+
except Exception:
|
|
179
|
+
return None
|
|
180
|
+
finally:
|
|
181
|
+
if sock is not None:
|
|
182
|
+
try:
|
|
183
|
+
sock.close()
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
def evaluate(ws_url, expression, timeout=1.5):
|
|
188
|
+
replies = ws_session(ws_url, [{"method": "Runtime.evaluate", "params": {"returnByValue": True, "expression": expression}}], timeout)
|
|
189
|
+
if not replies or not isinstance(replies[0], dict):
|
|
190
|
+
return None
|
|
191
|
+
result = replies[0].get("result") or {}
|
|
192
|
+
inner = result.get("result") if isinstance(result, dict) else None
|
|
193
|
+
return inner.get("value") if isinstance(inner, dict) else None
|
|
194
|
+
|
|
195
|
+
STATE_EXPRESSION = (
|
|
196
|
+
"({ url: location.href, title: document.title, "
|
|
197
|
+
"text: (document.body && document.body.innerText || '').slice(0, 20000), "
|
|
198
|
+
"scrollY: (window.scrollY || 0) })"
|
|
199
|
+
)
|
|
200
|
+
GEOMETRY_EXPRESSION = (
|
|
201
|
+
"({ browserWindow: { x: window.screenX, y: window.screenY, width: window.outerWidth, height: window.outerHeight }, "
|
|
202
|
+
"viewport: { width: window.innerWidth, height: window.innerHeight, deviceScaleFactor: window.devicePixelRatio } })"
|
|
203
|
+
)
|
|
204
|
+
FIDELITY_EXPRESSION = (
|
|
205
|
+
"({ userAgent: navigator.userAgent, devicePixelRatio: window.devicePixelRatio, "
|
|
206
|
+
"innerWidth: window.innerWidth, innerHeight: window.innerHeight, "
|
|
207
|
+
"maxTouchPoints: navigator.maxTouchPoints || 0, "
|
|
208
|
+
"coarsePointer: !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) })"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def emulate(ws_url, request, hold=False):
|
|
212
|
+
"""Apply the Emulation domain to the page and reload it so scripts that read the UA at load see it.
|
|
213
|
+
With hold=True the socket stays open (the overrides are bound to this session) until the process is killed."""
|
|
214
|
+
width = int(request.get("width") or 0)
|
|
215
|
+
height = int(request.get("height") or 0)
|
|
216
|
+
scale = float(request.get("deviceScaleFactor") or 1)
|
|
217
|
+
user_agent = str(request.get("userAgent") or "")
|
|
218
|
+
platform = str(request.get("platform") or "")
|
|
219
|
+
messages = [
|
|
220
|
+
{"method": "Emulation.setDeviceMetricsOverride", "params": {
|
|
221
|
+
"width": width, "height": height, "deviceScaleFactor": scale, "mobile": True,
|
|
222
|
+
"screenWidth": width, "screenHeight": height}},
|
|
223
|
+
]
|
|
224
|
+
if request.get("touch"):
|
|
225
|
+
messages.append({"method": "Emulation.setTouchEmulationEnabled", "params": {"enabled": True, "maxTouchPoints": 5}})
|
|
226
|
+
messages.append({"method": "Emulation.setEmitTouchEventsForMouse", "params": {"enabled": True, "configuration": "mobile"}})
|
|
227
|
+
if user_agent:
|
|
228
|
+
params = {"userAgent": user_agent}
|
|
229
|
+
if platform:
|
|
230
|
+
params["platform"] = platform
|
|
231
|
+
messages.append({"method": "Emulation.setUserAgentOverride", "params": params})
|
|
232
|
+
messages.append({"method": "Page.reload", "params": {}})
|
|
233
|
+
replies = ws_session(ws_url, messages, timeout=5, hold=hold)
|
|
234
|
+
if replies is None:
|
|
235
|
+
return None, "the page socket could not be opened"
|
|
236
|
+
applied = []
|
|
237
|
+
for message, reply in zip(messages, replies):
|
|
238
|
+
if isinstance(reply, dict) and "error" not in reply:
|
|
239
|
+
applied.append(message["method"])
|
|
240
|
+
else:
|
|
241
|
+
detail = (reply or {}).get("error", {}).get("message") if isinstance(reply, dict) else "no reply"
|
|
242
|
+
return applied, "%s failed: %s" % (message["method"], detail)
|
|
243
|
+
return applied, None
|
|
244
|
+
|
|
245
|
+
def main():
|
|
246
|
+
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
|
|
247
|
+
mode = args.get("mode") or "state"
|
|
248
|
+
port = resolve_port(args)
|
|
249
|
+
if mode == "port":
|
|
250
|
+
print(json.dumps({"cdpPort": port}))
|
|
251
|
+
return
|
|
252
|
+
try:
|
|
253
|
+
pages = list_pages(port)
|
|
254
|
+
except Exception as error:
|
|
255
|
+
print(json.dumps({"unavailable": "CDP endpoint 127.0.0.1:%d/json unreachable (%s)" % (port, type(error).__name__)}))
|
|
256
|
+
return
|
|
257
|
+
page = select_page(pages, args)
|
|
258
|
+
if page is None:
|
|
259
|
+
print(json.dumps({"unavailable": "no http page among %d CDP targets on 127.0.0.1:%d" % (len(pages), port)}))
|
|
260
|
+
return
|
|
261
|
+
ws_url = page.get("webSocketDebuggerUrl")
|
|
262
|
+
if mode == "emulate":
|
|
263
|
+
applied, failure = emulate(ws_url, args.get("emulation") or {}) if ws_url else (None, "the page has no socket")
|
|
264
|
+
if failure is not None:
|
|
265
|
+
print(json.dumps({"unavailable": failure, "applied": applied or []}))
|
|
266
|
+
return
|
|
267
|
+
print(json.dumps({"applied": applied, "targetId": str(page.get("id") or "")}))
|
|
268
|
+
return
|
|
269
|
+
if mode == "hold":
|
|
270
|
+
if not ws_url:
|
|
271
|
+
print(json.dumps({"unavailable": "the page has no socket"}), flush=True)
|
|
272
|
+
return
|
|
273
|
+
# Prints its own line from inside ws_session (before blocking) and never returns on success.
|
|
274
|
+
applied, failure = emulate(ws_url, args.get("emulation") or {}, hold=True)
|
|
275
|
+
if failure is not None:
|
|
276
|
+
print(json.dumps({"unavailable": failure, "applied": applied or []}), flush=True)
|
|
277
|
+
return
|
|
278
|
+
if mode == "fidelity":
|
|
279
|
+
result = evaluate(ws_url, FIDELITY_EXPRESSION) if ws_url else None
|
|
280
|
+
if not isinstance(result, dict):
|
|
281
|
+
print(json.dumps({"unavailable": "Runtime.evaluate over the page socket returned nothing"}))
|
|
282
|
+
return
|
|
283
|
+
print(json.dumps({"fidelity": result, "targetId": str(page.get("id") or "")}))
|
|
284
|
+
return
|
|
285
|
+
if mode == "geometry":
|
|
286
|
+
result = evaluate(ws_url, GEOMETRY_EXPRESSION) if ws_url else None
|
|
287
|
+
if not isinstance(result, dict):
|
|
288
|
+
print(json.dumps({"unavailable": "Runtime.evaluate over the page socket returned nothing"}))
|
|
289
|
+
return
|
|
290
|
+
result["targetId"] = str(page.get("id") or "")
|
|
291
|
+
print(json.dumps(result))
|
|
292
|
+
return
|
|
293
|
+
url = str(page.get("url") or "")
|
|
294
|
+
title = str(page.get("title") or "")
|
|
295
|
+
text = ""
|
|
296
|
+
scroll_y = None
|
|
297
|
+
result = evaluate(ws_url, STATE_EXPRESSION) if ws_url else None
|
|
298
|
+
if isinstance(result, dict):
|
|
299
|
+
url = result["url"] if isinstance(result.get("url"), str) else url
|
|
300
|
+
title = result["title"] if isinstance(result.get("title"), str) else title
|
|
301
|
+
text = result["text"] if isinstance(result.get("text"), str) else ""
|
|
302
|
+
scroll_y = result["scrollY"] if isinstance(result.get("scrollY"), (int, float)) else None
|
|
303
|
+
print(json.dumps({"url": url, "title": title, "text": text, "scrollY": scroll_y}))
|
|
304
|
+
|
|
305
|
+
main()
|
|
306
|
+
`;
|
|
307
|
+
function shellSingleQuote(value) {
|
|
308
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
309
|
+
}
|
|
310
|
+
/** The exact shell command a sandbox runs for one probe. */
|
|
311
|
+
export function chromeCdpProbeCommand(args) {
|
|
312
|
+
const payload = { mode: args.mode, targetUrl: args.targetUrl };
|
|
313
|
+
if (args.cdpPort !== undefined)
|
|
314
|
+
payload.cdpPort = args.cdpPort;
|
|
315
|
+
if (args.profileDir !== undefined)
|
|
316
|
+
payload.profileDir = args.profileDir;
|
|
317
|
+
if (args.targetId !== undefined)
|
|
318
|
+
payload.targetId = args.targetId;
|
|
319
|
+
if (args.prefer !== undefined)
|
|
320
|
+
payload.prefer = args.prefer;
|
|
321
|
+
if (args.emulation !== undefined)
|
|
322
|
+
payload.emulation = args.emulation;
|
|
323
|
+
return `python3 -c ${shellSingleQuote(CHROME_CDP_PROBE_PY)} ${shellSingleQuote(JSON.stringify(payload))}`;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Narrow one probe's stdout. A parse failure is reported as unavailable with the reason, never as
|
|
327
|
+
* an empty success: the difference between "nothing to observe" and "could not observe" is the
|
|
328
|
+
* whole point of #514.
|
|
329
|
+
*/
|
|
330
|
+
export function parseChromeCdpProbeOutput(stdout) {
|
|
331
|
+
const trimmed = (stdout ?? "").trim();
|
|
332
|
+
if (trimmed.length === 0)
|
|
333
|
+
return { unavailable: "probe printed nothing" };
|
|
334
|
+
let parsed;
|
|
335
|
+
try {
|
|
336
|
+
parsed = JSON.parse(trimmed);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return { unavailable: "probe output was not JSON" };
|
|
340
|
+
}
|
|
341
|
+
if (!parsed || typeof parsed !== "object")
|
|
342
|
+
return { unavailable: "probe output was not an object" };
|
|
343
|
+
const record = parsed;
|
|
344
|
+
const applied = Array.isArray(record.applied)
|
|
345
|
+
? record.applied.filter((item) => typeof item === "string")
|
|
346
|
+
: undefined;
|
|
347
|
+
if (typeof record.unavailable === "string") {
|
|
348
|
+
return { unavailable: record.unavailable, ...(applied === undefined ? {} : { applied }) };
|
|
349
|
+
}
|
|
350
|
+
const numberOr = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
351
|
+
const box = (value, keys) => {
|
|
352
|
+
if (!value || typeof value !== "object")
|
|
353
|
+
return undefined;
|
|
354
|
+
const source = value;
|
|
355
|
+
const out = {};
|
|
356
|
+
for (const key of keys) {
|
|
357
|
+
const n = numberOr(source[key]);
|
|
358
|
+
if (n === undefined)
|
|
359
|
+
return undefined;
|
|
360
|
+
out[key] = n;
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
};
|
|
364
|
+
const browserWindow = box(record.browserWindow, ["x", "y", "width", "height"]);
|
|
365
|
+
const viewport = box(record.viewport, ["width", "height", "deviceScaleFactor"]);
|
|
366
|
+
const cdpPort = numberOr(record.cdpPort);
|
|
367
|
+
const scrollY = numberOr(record.scrollY);
|
|
368
|
+
const fidelity = (() => {
|
|
369
|
+
const raw = record.fidelity;
|
|
370
|
+
if (!raw || typeof raw !== "object")
|
|
371
|
+
return undefined;
|
|
372
|
+
const source = raw;
|
|
373
|
+
const dpr = numberOr(source.devicePixelRatio);
|
|
374
|
+
const innerWidth = numberOr(source.innerWidth);
|
|
375
|
+
const innerHeight = numberOr(source.innerHeight);
|
|
376
|
+
const maxTouchPoints = numberOr(source.maxTouchPoints);
|
|
377
|
+
if (typeof source.userAgent !== "string" || dpr === undefined || innerWidth === undefined || innerHeight === undefined || maxTouchPoints === undefined) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
return { userAgent: source.userAgent, devicePixelRatio: dpr, innerWidth, innerHeight, maxTouchPoints, coarsePointer: source.coarsePointer === true };
|
|
381
|
+
})();
|
|
382
|
+
return {
|
|
383
|
+
...(applied === undefined ? {} : { applied }),
|
|
384
|
+
...(fidelity === undefined ? {} : { fidelity }),
|
|
385
|
+
...(cdpPort === undefined ? {} : { cdpPort }),
|
|
386
|
+
...(typeof record.url === "string" && record.url.length > 0 ? { url: record.url } : {}),
|
|
387
|
+
...(typeof record.title === "string" && record.title.length > 0 ? { title: record.title } : {}),
|
|
388
|
+
...(typeof record.text === "string" && record.text.length > 0 ? { text: record.text } : {}),
|
|
389
|
+
...(scrollY === undefined ? {} : { scrollY }),
|
|
390
|
+
...(typeof record.targetId === "string" && record.targetId.length > 0 ? { targetId: record.targetId } : {}),
|
|
391
|
+
...(browserWindow === undefined ? {} : { browserWindow }),
|
|
392
|
+
...(viewport === undefined ? {} : { viewport })
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
//# sourceMappingURL=chrome-cdp-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chrome-cdp-probe.js","sourceRoot":"","sources":["../src/chrome-cdp-probe.ts"],"names":[],"mappings":"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"}
|
package/dist/cua-actor-lab.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { type E2BDesktopModule, type E2BDesktopSandbox } from "./e2b-desktop-lau
|
|
|
10
10
|
import { type DetachedTimers } from "./e2b-detached.js";
|
|
11
11
|
import { type DevicePreset } from "./device-presets.js";
|
|
12
12
|
import { type LabActorLane, type LabCommsEmail, type LabCommsRecipient, type LabConfig, type LabDesktopBrowser, type LabSubjectServe, type LabSubjectState } from "./lab-config.js";
|
|
13
|
+
import { type ChromeMobileEmulationRequest } from "./chrome-cdp-probe.js";
|
|
13
14
|
import { type ResolvedPersona } from "./persona.js";
|
|
14
15
|
import { type LabTask } from "./tasks.js";
|
|
15
16
|
import { renderObserver, type ObserverResult } from "./observer.js";
|
|
@@ -661,9 +662,9 @@ export declare function desktopBrowserFamily(value: string | undefined): Desktop
|
|
|
661
662
|
/**
|
|
662
663
|
* Runtime-only CDP endpoint attribution for the exact chromium this lane launched. Port
|
|
663
664
|
* resolution at OBSERVE time: the cached launch-time `cdpPort` wins; absent that, the observer
|
|
664
|
-
*
|
|
665
|
+
* probe re-reads `profileDir`'s DevToolsActivePort marker (a slow cold start can publish it
|
|
665
666
|
* AFTER the launch-time poll gave up); absent both it falls back to the legacy fixed 9222,
|
|
666
|
-
* where a dead endpoint degrades into an honest warning.
|
|
667
|
+
* where a dead endpoint degrades into an honest warning that names the cause.
|
|
667
668
|
*/
|
|
668
669
|
export interface ChromeCdpEndpoint {
|
|
669
670
|
cdpPort?: number;
|
|
@@ -673,14 +674,18 @@ export interface ChromeCdpEndpoint {
|
|
|
673
674
|
targetUrl: string;
|
|
674
675
|
}
|
|
675
676
|
/**
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
677
|
+
* The URL / title / page-text / scroll observer behind stopWhen and task criteria. One probe per
|
|
678
|
+
* observation, run on the sandbox's python3 (see chrome-cdp-probe.ts for why not node: #514).
|
|
679
|
+
*
|
|
680
|
+
* "active": follow the participant to whatever tab they are driving now — never pin the state
|
|
681
|
+
* observer to the launch tab (a verification link that opened in a NEW tab left a pinned observer
|
|
682
|
+
* reading the old tab forever).
|
|
683
|
+
*
|
|
684
|
+
* `onUnavailable` fires ONCE, on the first probe that could not read the page, with the reason.
|
|
685
|
+
* The observer still degrades to `{}` for the loop; the callback is how a lane says out loud that
|
|
686
|
+
* url/text criteria are not being measured, instead of letting the funnel report 0/N (#514).
|
|
681
687
|
*/
|
|
682
|
-
export declare function
|
|
683
|
-
export declare function makeChromeBrowserStateObserver(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId?: string): () => Promise<{
|
|
688
|
+
export declare function makeChromeBrowserStateObserver(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId?: string, onUnavailable?: (reason: string) => void): () => Promise<{
|
|
684
689
|
url?: string;
|
|
685
690
|
title?: string;
|
|
686
691
|
text?: string;
|
|
@@ -690,10 +695,24 @@ export declare function makeChromeBrowserStateObserver(desktop: E2BDesktopSandbo
|
|
|
690
695
|
* Read the running browser's actual outer-window bounds and CSS layout viewport through the
|
|
691
696
|
* already-enabled local Chrome DevTools endpoint. The returned values come from `window.*` in
|
|
692
697
|
* the target page; requested E2B resolution is deliberately not an input to this function.
|
|
698
|
+
* `undefined` carries the reason the measurement is missing via `onUnavailable`, so the geometry
|
|
699
|
+
* warning can name the cause (a dead CDP endpoint, no python3) instead of only the symptom.
|
|
693
700
|
*/
|
|
694
|
-
export declare function makeChromeDesktopGeometryObserver(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId?: string): () => Promise<(Pick<RunDesktopGeometry, "browserWindow" | "viewport"> & {
|
|
701
|
+
export declare function makeChromeDesktopGeometryObserver(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId?: string, onUnavailable?: (reason: string) => void): () => Promise<(Pick<RunDesktopGeometry, "browserWindow" | "viewport"> & {
|
|
695
702
|
targetId?: string;
|
|
696
703
|
}) | undefined>;
|
|
704
|
+
/** The user agent a mobile-emulated lane presents unless the lab sets its own. */
|
|
705
|
+
export declare const DEFAULT_MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
|
|
706
|
+
/**
|
|
707
|
+
* Apply mobile emulation (#221) to the lane's launch page and read back what the page reports.
|
|
708
|
+
* Fails CLOSED: a request that cannot be applied throws, because a desktop run labelled mobile is
|
|
709
|
+
* the over-trust this feature exists to prevent. A read-back that cannot be taken is a warning
|
|
710
|
+
* (the emulation was applied; only the proof is missing).
|
|
711
|
+
*/
|
|
712
|
+
export declare function applyMobileEmulation(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId: string | undefined, request: ChromeMobileEmulationRequest): Promise<{
|
|
713
|
+
fidelity: NonNullable<RunDesktopGeometry["fidelity"]>;
|
|
714
|
+
warnings: string[];
|
|
715
|
+
}>;
|
|
697
716
|
/** Shared hosted-browser geometry capture used by per-lane and sequential shared-world routes. */
|
|
698
717
|
export declare function captureDesktopBrowserGeometry(args: {
|
|
699
718
|
desktop: E2BDesktopSandbox;
|