humanish 0.76.0 → 0.78.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 +6 -4
- package/dist/chrome-cdp-probe.d.ts +4 -2
- package/dist/chrome-cdp-probe.js +238 -110
- package/dist/chrome-cdp-probe.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +22 -1
- package/dist/cua-actor-lab.js +94 -8
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/e2b-desktop-launch.d.ts +33 -1
- package/dist/e2b-desktop-launch.js +49 -4
- package/dist/e2b-desktop-launch.js.map +1 -1
- package/dist/openai-responses-cu.d.ts +14 -0
- package/dist/openai-responses-cu.js +55 -2
- package/dist/openai-responses-cu.js.map +1 -1
- package/dist/run.d.ts +27 -0
- package/dist/run.js +37 -1
- package/dist/run.js.map +1 -1
- package/docs/contracts/schemas.md +1 -1
- package/docs/goals/current.md +38 -5
- package/docs/ramp/README.md +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -53,8 +53,8 @@ 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
|
|
57
|
-
|
|
56
|
+
**What it finds, measured (2026-09-01 to 09-04, receipts in `bench/` and `docs/goals/`):** 58 of
|
|
57
|
+
60 planted defects over four benchmark runs on an app we wrote, none invented in 15 clean runs; on
|
|
58
58
|
two apps we did not write, 16 of 18 distinct findings confirmed against the source and 0 invented,
|
|
59
59
|
across 14 participants; a drawDB modal reported by 5 of 5 keyboard-first participants (3 stopped
|
|
60
60
|
there) and never mentioned by 5 mouse-driving newcomers; a TodoMVC rename that blocked 6 of 6
|
|
@@ -483,8 +483,10 @@ and `resolved`: what the page itself reported afterwards (`navigator.userAgent`,
|
|
|
483
483
|
`devicePixelRatio`, `innerWidth`, `maxTouchPoints`, coarse pointer). A page without a viewport
|
|
484
484
|
meta lays out at 980 px, as it would on a phone, and the bundle says so. Firefox cannot be
|
|
485
485
|
emulated, so the lane fails closed instead of shipping a desktop run labelled mobile. The
|
|
486
|
-
|
|
487
|
-
|
|
486
|
+
viewport and DPR override cover the launch tab (the user agent and touch flags are browser-wide);
|
|
487
|
+
if an observation reads a tab the participant opened later, the lane records one warning saying
|
|
488
|
+
so, because that tab laid out at the window width. A bundle without a `fidelity` block is a
|
|
489
|
+
responsive-viewport study whatever its preset is called.
|
|
488
490
|
|
|
489
491
|
**Desktop browser choice.** Hosted computer-use lanes and shared-world actor seats use the
|
|
490
492
|
route's historical opener unless you set `execution.desktop.browser` to `chrome`, `chromium`,
|
|
@@ -21,8 +21,10 @@ export interface ChromeCdpProbeArgs {
|
|
|
21
21
|
/**
|
|
22
22
|
* "state": url/title/text/scrollY. "geometry": outer window + CSS viewport. "port": resolution
|
|
23
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
|
|
25
|
-
*
|
|
24
|
+
* are session-scoped, UA / touch / DPR, lapse when the socket closes). "hold": the same over a browser-level
|
|
25
|
+
* socket, then stay attached until killed (how a lane keeps them for its whole life) and attach
|
|
26
|
+
* to every page target Chrome opens later, sending it the same overrides and a reload the moment
|
|
27
|
+
* it exists, never pausing it (#623). "fidelity": read
|
|
26
28
|
* back what the page reports about itself (UA, DPR, viewport, touch), the proof for the bundle.
|
|
27
29
|
*/
|
|
28
30
|
mode: "state" | "geometry" | "port" | "emulate" | "hold" | "fidelity";
|
package/dist/chrome-cdp-probe.js
CHANGED
|
@@ -73,116 +73,128 @@ def select_page(pages, args):
|
|
|
73
73
|
return match
|
|
74
74
|
return http_pages[0] if len(http_pages) == 1 else None
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
sock
|
|
86
|
-
sock.settimeout(timeout)
|
|
76
|
+
class Ws:
|
|
77
|
+
"""One DevTools WebSocket: hand-rolled client frames, JSON in and out; events that arrive while
|
|
78
|
+
a reply is awaited are kept in .events for the caller."""
|
|
79
|
+
def __init__(self, ws_url, timeout=1.5):
|
|
80
|
+
match = re.match(r"^ws://([^/:]+):(\d+)(/.*)$", str(ws_url or ""))
|
|
81
|
+
if not match:
|
|
82
|
+
raise ValueError("not a ws url")
|
|
83
|
+
host, port, path = match.group(1), int(match.group(2)), match.group(3)
|
|
84
|
+
self.sock = socket.create_connection((host, port), timeout=timeout)
|
|
85
|
+
self.sock.settimeout(timeout)
|
|
87
86
|
key = base64.b64encode(os.urandom(16)).decode("ascii")
|
|
88
|
-
|
|
87
|
+
self.sock.sendall((
|
|
89
88
|
"GET %s HTTP/1.1\r\nHost: %s:%d\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
|
90
89
|
"Sec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n\r\n" % (path, host, port, key)
|
|
91
|
-
)
|
|
92
|
-
sock.sendall(handshake.encode("ascii"))
|
|
90
|
+
).encode("ascii"))
|
|
93
91
|
buffer = b""
|
|
94
92
|
while b"\r\n\r\n" not in buffer:
|
|
95
|
-
chunk = sock.recv(4096)
|
|
93
|
+
chunk = self.sock.recv(4096)
|
|
96
94
|
if not chunk:
|
|
97
|
-
|
|
95
|
+
raise EOFError("closed during the handshake")
|
|
98
96
|
buffer += chunk
|
|
99
|
-
head, buffer = buffer.split(b"\r\n\r\n", 1)
|
|
97
|
+
head, self.buffer = buffer.split(b"\r\n\r\n", 1)
|
|
100
98
|
if b" 101 " not in head.split(b"\r\n", 1)[0]:
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
raise EOFError("handshake refused")
|
|
100
|
+
self.next_id = 0
|
|
101
|
+
self.events = []
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
def need(self, count):
|
|
104
|
+
while len(self.buffer) < count:
|
|
105
|
+
chunk = self.sock.recv(65536)
|
|
106
|
+
if not chunk:
|
|
107
|
+
raise EOFError("socket closed")
|
|
108
|
+
self.buffer += chunk
|
|
110
109
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
frame +=
|
|
125
|
-
|
|
110
|
+
def send(self, method, params=None, session_id=None):
|
|
111
|
+
self.next_id += 1
|
|
112
|
+
message = {"id": self.next_id, "method": method, "params": params or {}}
|
|
113
|
+
if session_id:
|
|
114
|
+
message["sessionId"] = session_id
|
|
115
|
+
payload = json.dumps(message).encode("utf-8")
|
|
116
|
+
mask = os.urandom(4)
|
|
117
|
+
frame = bytearray([0x81])
|
|
118
|
+
size = len(payload)
|
|
119
|
+
if size < 126:
|
|
120
|
+
frame.append(0x80 | size)
|
|
121
|
+
elif size < 65536:
|
|
122
|
+
frame.append(0x80 | 126)
|
|
123
|
+
frame += struct.pack(">H", size)
|
|
124
|
+
else:
|
|
125
|
+
frame.append(0x80 | 127)
|
|
126
|
+
frame += struct.pack(">Q", size)
|
|
127
|
+
frame += mask + bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload))
|
|
128
|
+
self.sock.sendall(bytes(frame))
|
|
129
|
+
return self.next_id
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
def recv(self):
|
|
132
|
+
"""The next JSON message (a reply or an event), or None once the socket closes."""
|
|
133
|
+
message = b""
|
|
134
|
+
while True:
|
|
135
|
+
self.need(2)
|
|
136
|
+
first, second = self.buffer[0], self.buffer[1]
|
|
137
|
+
fin, opcode = first & 0x80, first & 0x0F
|
|
138
|
+
masked, length, offset = second & 0x80, second & 0x7F, 2
|
|
139
|
+
if length == 126:
|
|
140
|
+
self.need(4)
|
|
141
|
+
length, offset = struct.unpack(">H", self.buffer[2:4])[0], 4
|
|
142
|
+
elif length == 127:
|
|
143
|
+
self.need(10)
|
|
144
|
+
length, offset = struct.unpack(">Q", self.buffer[2:10])[0], 10
|
|
145
|
+
frame_mask = b""
|
|
146
|
+
if masked:
|
|
147
|
+
self.need(offset + 4)
|
|
148
|
+
frame_mask, offset = self.buffer[offset:offset + 4], offset + 4
|
|
149
|
+
self.need(offset + length)
|
|
150
|
+
data = self.buffer[offset:offset + length]
|
|
151
|
+
self.buffer = self.buffer[offset + length:]
|
|
152
|
+
if masked:
|
|
153
|
+
data = bytes(byte ^ frame_mask[index % 4] for index, byte in enumerate(data))
|
|
154
|
+
if opcode == 8:
|
|
155
|
+
return None
|
|
156
|
+
if opcode in (9, 10):
|
|
157
|
+
continue
|
|
158
|
+
message += data
|
|
159
|
+
if fin:
|
|
160
|
+
try:
|
|
161
|
+
reply = json.loads(message.decode("utf-8"))
|
|
162
|
+
except Exception:
|
|
150
163
|
return None
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
164
|
+
message = b""
|
|
165
|
+
if isinstance(reply, dict):
|
|
166
|
+
return reply
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
for
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
168
|
+
def call(self, method, params=None, session_id=None):
|
|
169
|
+
"""Send one command and wait for its reply; events seen on the way are queued."""
|
|
170
|
+
wanted = self.send(method, params, session_id)
|
|
171
|
+
while True:
|
|
172
|
+
reply = self.recv()
|
|
173
|
+
if reply is None:
|
|
174
|
+
return None
|
|
175
|
+
if reply.get("id") == wanted:
|
|
176
|
+
return reply
|
|
177
|
+
if "method" in reply:
|
|
178
|
+
self.events.append(reply)
|
|
179
|
+
|
|
180
|
+
def close(self):
|
|
181
|
+
try:
|
|
182
|
+
self.sock.close()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
def ws_session(ws_url, messages, timeout=1.5):
|
|
187
|
+
"""Send CDP messages over one page socket, in order, and return their replies (None on failure)."""
|
|
188
|
+
try:
|
|
189
|
+
ws = Ws(ws_url, timeout)
|
|
190
|
+
except Exception:
|
|
191
|
+
return None
|
|
192
|
+
try:
|
|
193
|
+
return [ws.call(message["method"], message.get("params")) for message in messages]
|
|
178
194
|
except Exception:
|
|
179
195
|
return None
|
|
180
196
|
finally:
|
|
181
|
-
|
|
182
|
-
try:
|
|
183
|
-
sock.close()
|
|
184
|
-
except Exception:
|
|
185
|
-
pass
|
|
197
|
+
ws.close()
|
|
186
198
|
|
|
187
199
|
def evaluate(ws_url, expression, timeout=1.5):
|
|
188
200
|
replies = ws_session(ws_url, [{"method": "Runtime.evaluate", "params": {"returnByValue": True, "expression": expression}}], timeout)
|
|
@@ -208,9 +220,8 @@ FIDELITY_EXPRESSION = (
|
|
|
208
220
|
"coarsePointer: !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) })"
|
|
209
221
|
)
|
|
210
222
|
|
|
211
|
-
def
|
|
212
|
-
"""
|
|
213
|
-
With hold=True the socket stays open (the overrides are bound to this session) until the process is killed."""
|
|
223
|
+
def emulation_messages(request, reload):
|
|
224
|
+
"""The Emulation domain for one page session; reload only where scripts already ran at load."""
|
|
214
225
|
width = int(request.get("width") or 0)
|
|
215
226
|
height = int(request.get("height") or 0)
|
|
216
227
|
scale = float(request.get("deviceScaleFactor") or 1)
|
|
@@ -229,10 +240,11 @@ def emulate(ws_url, request, hold=False):
|
|
|
229
240
|
if platform:
|
|
230
241
|
params["platform"] = platform
|
|
231
242
|
messages.append({"method": "Emulation.setUserAgentOverride", "params": params})
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
243
|
+
if reload:
|
|
244
|
+
messages.append({"method": "Page.reload", "params": {}})
|
|
245
|
+
return messages
|
|
246
|
+
|
|
247
|
+
def applied_of(messages, replies):
|
|
236
248
|
applied = []
|
|
237
249
|
for message, reply in zip(messages, replies):
|
|
238
250
|
if isinstance(reply, dict) and "error" not in reply:
|
|
@@ -242,6 +254,122 @@ def emulate(ws_url, request, hold=False):
|
|
|
242
254
|
return applied, "%s failed: %s" % (message["method"], detail)
|
|
243
255
|
return applied, None
|
|
244
256
|
|
|
257
|
+
def emulate(ws_url, request):
|
|
258
|
+
"""One-shot: apply the Emulation domain to the page and reload it. The session-scoped overrides
|
|
259
|
+
lapse when this socket closes; the lane uses hold() instead."""
|
|
260
|
+
messages = emulation_messages(request, reload=True)
|
|
261
|
+
replies = ws_session(ws_url, messages, timeout=5)
|
|
262
|
+
if replies is None:
|
|
263
|
+
return None, "the page socket could not be opened"
|
|
264
|
+
return applied_of(messages, replies)
|
|
265
|
+
|
|
266
|
+
def apply_over(ws, request, session_id, reload):
|
|
267
|
+
messages = emulation_messages(request, reload)
|
|
268
|
+
return applied_of(messages, [ws.call(m["method"], m.get("params"), session_id) for m in messages])
|
|
269
|
+
|
|
270
|
+
def browser_ws_url(port):
|
|
271
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
272
|
+
with opener.open("http://127.0.0.1:%d/json/version" % port, timeout=2) as response:
|
|
273
|
+
info = json.loads(response.read().decode("utf-8"))
|
|
274
|
+
return str(info.get("webSocketDebuggerUrl") or "")
|
|
275
|
+
|
|
276
|
+
def hold(port, page, request):
|
|
277
|
+
"""Emulate the launch page and then every page target Chrome opens later, for as long as this
|
|
278
|
+
process lives (#221, #623). One browser-level socket with flattened sessions: the launch page is
|
|
279
|
+
attached by id and reloaded so scripts that read the UA at load see it; a target that appears
|
|
280
|
+
later is attached the moment it exists, sent the same overrides, and reloaded once after its
|
|
281
|
+
first real navigation commits (touch emulation reaches a document only when it loads under it).
|
|
282
|
+
Two things this loop must never do (measured on a real desktop, 2026-09-04): pause new targets
|
|
283
|
+
(waitForDebuggerOnStart) and wait for their replies. A popup a participant taps open shares its
|
|
284
|
+
opener's renderer, answers Emulation commands only once it runs, and a loop blocked on that
|
|
285
|
+
reply never resumed the tab (it loaded forever) and never reached the next one. Overrides are
|
|
286
|
+
sent without waiting; replies, errors included, are logged as they arrive.
|
|
287
|
+
Prints one JSON line per attached target; the first line is the announce the lane reads."""
|
|
288
|
+
launch_id = str(page.get("id") or "")
|
|
289
|
+
try:
|
|
290
|
+
ws = Ws(browser_ws_url(port), timeout=5)
|
|
291
|
+
except Exception as error:
|
|
292
|
+
print(json.dumps({"unavailable": "the browser socket could not be opened (%s)" % type(error).__name__}), flush=True)
|
|
293
|
+
return
|
|
294
|
+
attach = ws.call("Target.attachToTarget", {"targetId": launch_id, "flatten": True})
|
|
295
|
+
session_id = ((attach or {}).get("result") or {}).get("sessionId") if isinstance(attach, dict) else None
|
|
296
|
+
if not session_id:
|
|
297
|
+
print(json.dumps({"unavailable": "Target.attachToTarget failed for the launch page"}), flush=True)
|
|
298
|
+
return
|
|
299
|
+
covered = {launch_id}
|
|
300
|
+
applied, failure = apply_over(ws, request, session_id, reload=True)
|
|
301
|
+
print(json.dumps({"applied": applied, "held": failure is None, "targetId": launch_id, **({"unavailable": failure} if failure else {})}), flush=True)
|
|
302
|
+
if failure:
|
|
303
|
+
return
|
|
304
|
+
auto = ws.call("Target.setAutoAttach", {"autoAttach": True, "waitForDebuggerOnStart": False, "flatten": True})
|
|
305
|
+
if not isinstance(auto, dict) or "error" in auto:
|
|
306
|
+
print(json.dumps({"autoAttach": False, "unavailable": "Target.setAutoAttach failed; later tabs are not emulated"}), flush=True)
|
|
307
|
+
ws.sock.settimeout(None)
|
|
308
|
+
pending = {}
|
|
309
|
+
# Sessions of later targets that still owe one reload: touch emulation reaches a document only
|
|
310
|
+
# when it loads under the override (measured: a popup's first document reported
|
|
311
|
+
# maxTouchPoints 0 with the viewport already 414 px), so each later tab is reloaded once after
|
|
312
|
+
# its first real navigation commits, or at once when it had already committed at attach time.
|
|
313
|
+
reload_owed = {}
|
|
314
|
+
while True:
|
|
315
|
+
try:
|
|
316
|
+
message = ws.events.pop(0) if ws.events else ws.recv()
|
|
317
|
+
except Exception as error:
|
|
318
|
+
print(json.dumps({"holderExit": "%s: %s" % (type(error).__name__, error)}), flush=True)
|
|
319
|
+
return
|
|
320
|
+
if message is None:
|
|
321
|
+
print(json.dumps({"holderExit": "browser socket closed"}), flush=True)
|
|
322
|
+
return
|
|
323
|
+
reply_id = message.get("id")
|
|
324
|
+
session = message.get("sessionId")
|
|
325
|
+
if reply_id in pending:
|
|
326
|
+
method = pending.pop(reply_id)
|
|
327
|
+
if "error" in message:
|
|
328
|
+
print(json.dumps({"replyError": method, "message": str((message.get("error") or {}).get("message"))}), flush=True)
|
|
329
|
+
elif method == "href" and session in reload_owed:
|
|
330
|
+
# The navigation may have committed before Page.enable could report it; the page's
|
|
331
|
+
# own location says so, and the reload it is owed goes out now (once: pop guards it).
|
|
332
|
+
href = str((((message.get("result") or {}).get("result") or {}).get("value")) or "")
|
|
333
|
+
if href and not href.startswith("about:"):
|
|
334
|
+
target_id = reload_owed.pop(session)
|
|
335
|
+
pending[ws.send("Page.reload", {}, session)] = "Page.reload"
|
|
336
|
+
print(json.dumps({"reloaded": target_id, "url": href, "by": "href"}), flush=True)
|
|
337
|
+
continue
|
|
338
|
+
method = message.get("method")
|
|
339
|
+
params = message.get("params") or {}
|
|
340
|
+
if method == "Page.frameNavigated" and session in reload_owed:
|
|
341
|
+
frame = params.get("frame") or {}
|
|
342
|
+
url = str(frame.get("url") or "")
|
|
343
|
+
if not frame.get("parentId") and url and not url.startswith("about:"):
|
|
344
|
+
target_id = reload_owed.pop(session)
|
|
345
|
+
pending[ws.send("Page.reload", {}, session)] = "Page.reload"
|
|
346
|
+
print(json.dumps({"reloaded": target_id, "url": url}), flush=True)
|
|
347
|
+
continue
|
|
348
|
+
if method != "Target.attachedToTarget":
|
|
349
|
+
continue
|
|
350
|
+
info = params.get("targetInfo") or {}
|
|
351
|
+
new_session = params.get("sessionId")
|
|
352
|
+
target_id = str(info.get("targetId") or "")
|
|
353
|
+
if info.get("type") == "page" and target_id and target_id not in covered:
|
|
354
|
+
covered.add(target_id)
|
|
355
|
+
messages = emulation_messages(request, reload=False)
|
|
356
|
+
for m in messages:
|
|
357
|
+
pending[ws.send(m["method"], m.get("params"), new_session)] = m["method"]
|
|
358
|
+
pending[ws.send("Page.enable", {}, new_session)] = "Page.enable"
|
|
359
|
+
url = str(info.get("url") or "")
|
|
360
|
+
if url and not url.startswith("about:"):
|
|
361
|
+
pending[ws.send("Page.reload", {}, new_session)] = "Page.reload"
|
|
362
|
+
print(json.dumps({"attached": target_id, "sent": [m["method"] for m in messages] + ["Page.reload"], "url": url}), flush=True)
|
|
363
|
+
else:
|
|
364
|
+
reload_owed[new_session] = target_id
|
|
365
|
+
# Two ways to learn the first real navigation committed, whichever answers first:
|
|
366
|
+
# Page.frameNavigated (if Page.enable landed before the commit) or the page's own
|
|
367
|
+
# location.href (if it did not).
|
|
368
|
+
pending[ws.send("Runtime.evaluate", {"expression": "location.href", "returnByValue": True}, new_session)] = "href"
|
|
369
|
+
print(json.dumps({"attached": target_id, "sent": [m["method"] for m in messages], "reloadAfterNavigation": True}), flush=True)
|
|
370
|
+
if params.get("waitingForDebugger"):
|
|
371
|
+
ws.send("Runtime.runIfWaitingForDebugger", {}, new_session)
|
|
372
|
+
|
|
245
373
|
def main():
|
|
246
374
|
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
|
|
247
375
|
mode = args.get("mode") or "state"
|
|
@@ -258,7 +386,11 @@ def main():
|
|
|
258
386
|
if page is None:
|
|
259
387
|
print(json.dumps({"unavailable": "no http page among %d CDP targets on 127.0.0.1:%d" % (len(pages), port)}))
|
|
260
388
|
return
|
|
261
|
-
|
|
389
|
+
# Chrome omits webSocketDebuggerUrl from /json while another DevTools client is attached to the
|
|
390
|
+
# page, or for a moment after one detaches; the page socket URL is still /devtools/page/<id>.
|
|
391
|
+
ws_url = page.get("webSocketDebuggerUrl") or (
|
|
392
|
+
"ws://127.0.0.1:%d/devtools/page/%s" % (port, page.get("id")) if page.get("id") else None
|
|
393
|
+
)
|
|
262
394
|
if mode == "emulate":
|
|
263
395
|
applied, failure = emulate(ws_url, args.get("emulation") or {}) if ws_url else (None, "the page has no socket")
|
|
264
396
|
if failure is not None:
|
|
@@ -267,13 +399,9 @@ def main():
|
|
|
267
399
|
print(json.dumps({"applied": applied, "targetId": str(page.get("id") or "")}))
|
|
268
400
|
return
|
|
269
401
|
if mode == "hold":
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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)
|
|
402
|
+
# Prints its announce line, then stays attached (and attaches to every later page target)
|
|
403
|
+
# until killed.
|
|
404
|
+
hold(port, page, args.get("emulation") or {})
|
|
277
405
|
return
|
|
278
406
|
if mode == "fidelity":
|
|
279
407
|
result = evaluate(ws_url, FIDELITY_EXPRESSION) if ws_url else None
|
|
@@ -300,7 +428,7 @@ def main():
|
|
|
300
428
|
title = result["title"] if isinstance(result.get("title"), str) else title
|
|
301
429
|
text = result["text"] if isinstance(result.get("text"), str) else ""
|
|
302
430
|
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}))
|
|
431
|
+
print(json.dumps({"url": url, "title": title, "text": text, "scrollY": scroll_y, "targetId": str(page.get("id") or "")}))
|
|
304
432
|
|
|
305
433
|
main()
|
|
306
434
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chrome-cdp-probe.js","sourceRoot":"","sources":["../src/chrome-cdp-probe.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,EAAE;AACF,iGAAiG;AACjG,8FAA8F;AAC9F,8FAA8F;AAC9F,kGAAkG;AAClG,0FAA0F;AAC1F,kGAAkG;AAClG,gGAAgG;AAChG,mGAAmG;AACnG,oDAAoD;AACpD,EAAE;AACF,kGAAkG;AAClG,4FAA4F;AAC5F,EAAE;AACF,8EAA8E;AAC9E,kGAAkG;AAClG,gGAAgG;AAChG,iEAAiE;
|
|
1
|
+
{"version":3,"file":"chrome-cdp-probe.js","sourceRoot":"","sources":["../src/chrome-cdp-probe.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,EAAE;AACF,iGAAiG;AACjG,8FAA8F;AAC9F,8FAA8F;AAC9F,kGAAkG;AAClG,0FAA0F;AAC1F,kGAAkG;AAClG,gGAAgG;AAChG,mGAAmG;AACnG,oDAAoD;AACpD,EAAE;AACF,kGAAkG;AAClG,4FAA4F;AAC5F,EAAE;AACF,8EAA8E;AAC9E,kGAAkG;AAClG,gGAAgG;AAChG,iEAAiE;AA0EjE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoZ5C,CAAC;AAEF,SAAS,gBAAgB,CAAC,KAAa;IACrC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AAC7C,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,qBAAqB,CAAC,IAAwB;IAC5D,MAAM,OAAO,GAA4B,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;IACxF,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC/D,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;QAAE,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;IACxE,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAClE,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5D,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;QAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACrE,OAAO,cAAc,gBAAgB,CAAC,mBAAmB,CAAC,IAAI,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;AAC5G,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAA0B;IAClE,MAAM,OAAO,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC;IAC1E,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;IACpG,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;QAC3C,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC;QAC3E,CAAC,CAAC,SAAS,CAAC;IACd,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;QAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAC5F,CAAC;IACD,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAsB,EAAE,CACtD,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1E,MAAM,GAAG,GAAG,CAAC,KAAc,EAAE,IAAc,EAAsC,EAAE;QACjF,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAC1D,MAAM,MAAM,GAAG,KAAgC,CAAC;QAChD,MAAM,GAAG,GAA2B,EAAE,CAAC;QACvC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAChC,IAAI,CAAC,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YACtC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACf,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;IACF,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,CAA0C,CAAC;IACxH,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,mBAAmB,CAAC,CAAqC,CAAC;IACpH,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,CAAC,GAAmC,EAAE;QACrD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC;QAC5B,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACtD,MAAM,MAAM,GAAG,GAA8B,CAAC;QAC9C,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAC9C,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC/C,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACjD,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QACvD,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,IAAI,GAAG,KAAK,SAAS,IAAI,UAAU,KAAK,SAAS,IAAI,WAAW,KAAK,SAAS,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;YACvJ,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;IACvJ,CAAC,CAAC,EAAE,CAAC;IACL,OAAO;QACL,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;QAC/C,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,GAAG,CAAC,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,GAAG,CAAC,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,GAAG,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3F,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,GAAG,CAAC,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3G,GAAG,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC;QACzD,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;KAChD,CAAC;AACJ,CAAC"}
|
package/dist/cua-actor-lab.d.ts
CHANGED
|
@@ -685,7 +685,26 @@ export interface ChromeCdpEndpoint {
|
|
|
685
685
|
* The observer still degrades to `{}` for the loop; the callback is how a lane says out loud that
|
|
686
686
|
* url/text criteria are not being measured, instead of letting the funnel report 0/N (#514).
|
|
687
687
|
*/
|
|
688
|
-
export declare function makeChromeBrowserStateObserver(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId?: string, onUnavailable?: (reason: string) => void
|
|
688
|
+
export declare function makeChromeBrowserStateObserver(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId?: string, onUnavailable?: (reason: string) => void,
|
|
689
|
+
/**
|
|
690
|
+
* Mobile emulation on later tabs (#623): the holder attaches to every page target Chrome opens
|
|
691
|
+
* after the launch page, so a tab the participant opens later should lay out at the phone width
|
|
692
|
+
* too. The first observation on each new target reads that page's OWN report; a target that
|
|
693
|
+
* reports the requested width is recorded through `onCovered`, and one that does not (or cannot
|
|
694
|
+
* be read) fires `onDrift` once, so a phone-labelled lane that spent part of its session at
|
|
695
|
+
* desktop layout says so with the number the page gave.
|
|
696
|
+
*/
|
|
697
|
+
drift?: {
|
|
698
|
+
emulatedTargetId: string;
|
|
699
|
+
expectedWidth: number;
|
|
700
|
+
expectTouch?: boolean;
|
|
701
|
+
onDrift: (reason: string) => void;
|
|
702
|
+
onCovered?: (targetId: string, read: {
|
|
703
|
+
innerWidth: number;
|
|
704
|
+
devicePixelRatio: number;
|
|
705
|
+
maxTouchPoints: number;
|
|
706
|
+
}) => void;
|
|
707
|
+
}): () => Promise<{
|
|
689
708
|
url?: string;
|
|
690
709
|
title?: string;
|
|
691
710
|
text?: string;
|
|
@@ -712,6 +731,8 @@ export declare const DEFAULT_MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhon
|
|
|
712
731
|
export declare function applyMobileEmulation(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId: string | undefined, request: ChromeMobileEmulationRequest): Promise<{
|
|
713
732
|
fidelity: NonNullable<RunDesktopGeometry["fidelity"]>;
|
|
714
733
|
warnings: string[];
|
|
734
|
+
targetId?: string;
|
|
735
|
+
holderName: string;
|
|
715
736
|
}>;
|
|
716
737
|
/** Shared hosted-browser geometry capture used by per-lane and sequential shared-world routes. */
|
|
717
738
|
export declare function captureDesktopBrowserGeometry(args: {
|