humanish 0.75.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 +32 -2
- package/dist/chrome-cdp-probe.js +165 -58
- package/dist/chrome-cdp-probe.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +13 -0
- package/dist/cua-actor-lab.js +128 -6
- 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/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 +13 -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
|
|
@@ -18,8 +18,34 @@ export interface ChromeCdpProbeArgs {
|
|
|
18
18
|
* funnel read reach-dashboard 0/2 under a screenshot OF the dashboard).
|
|
19
19
|
*/
|
|
20
20
|
prefer?: ChromeCdpPagePreference;
|
|
21
|
-
/**
|
|
22
|
-
|
|
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;
|
|
23
49
|
}
|
|
24
50
|
/** The probe's stdout, before the caller narrows it. */
|
|
25
51
|
export interface ChromeCdpProbeResult {
|
|
@@ -41,6 +67,10 @@ export interface ChromeCdpProbeResult {
|
|
|
41
67
|
height: number;
|
|
42
68
|
deviceScaleFactor: number;
|
|
43
69
|
};
|
|
70
|
+
/** "emulate": the CDP methods that returned without error, in order. */
|
|
71
|
+
applied?: string[];
|
|
72
|
+
/** "fidelity": the read-back. */
|
|
73
|
+
fidelity?: ChromeFidelityRead;
|
|
44
74
|
}
|
|
45
75
|
/**
|
|
46
76
|
* The probe itself. Kept as one string so the shipped command is exactly what the tests execute
|
package/dist/chrome-cdp-probe.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* a loopback read.
|
|
29
29
|
*/
|
|
30
30
|
export const CHROME_CDP_PROBE_PY = String.raw `
|
|
31
|
-
import base64, json, os, re, socket, struct, sys, urllib.request
|
|
31
|
+
import base64, json, os, re, socket, struct, sys, time, urllib.request
|
|
32
32
|
|
|
33
33
|
def resolve_port(args):
|
|
34
34
|
port = args.get("cdpPort")
|
|
@@ -73,7 +73,9 @@ 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
|
-
def
|
|
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."""
|
|
77
79
|
match = re.match(r"^ws://([^/:]+):(\d+)(/.*)$", str(ws_url or ""))
|
|
78
80
|
if not match:
|
|
79
81
|
return None
|
|
@@ -97,25 +99,6 @@ def evaluate(ws_url, expression, timeout=1.5):
|
|
|
97
99
|
head, buffer = buffer.split(b"\r\n\r\n", 1)
|
|
98
100
|
if b" 101 " not in head.split(b"\r\n", 1)[0]:
|
|
99
101
|
return None
|
|
100
|
-
payload = json.dumps({
|
|
101
|
-
"id": 1,
|
|
102
|
-
"method": "Runtime.evaluate",
|
|
103
|
-
"params": {"returnByValue": True, "expression": expression},
|
|
104
|
-
}).encode("utf-8")
|
|
105
|
-
mask = os.urandom(4)
|
|
106
|
-
frame = bytearray([0x81])
|
|
107
|
-
size = len(payload)
|
|
108
|
-
if size < 126:
|
|
109
|
-
frame.append(0x80 | size)
|
|
110
|
-
elif size < 65536:
|
|
111
|
-
frame.append(0x80 | 126)
|
|
112
|
-
frame += struct.pack(">H", size)
|
|
113
|
-
else:
|
|
114
|
-
frame.append(0x80 | 127)
|
|
115
|
-
frame += struct.pack(">Q", size)
|
|
116
|
-
frame += mask + bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload))
|
|
117
|
-
sock.sendall(bytes(frame))
|
|
118
|
-
|
|
119
102
|
state = {"buffer": buffer}
|
|
120
103
|
|
|
121
104
|
def need(count):
|
|
@@ -125,42 +108,73 @@ def evaluate(ws_url, expression, timeout=1.5):
|
|
|
125
108
|
raise EOFError("socket closed")
|
|
126
109
|
state["buffer"] += chunk
|
|
127
110
|
|
|
128
|
-
message
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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:
|
|
158
150
|
return None
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
164
178
|
except Exception:
|
|
165
179
|
return None
|
|
166
180
|
finally:
|
|
@@ -170,6 +184,14 @@ def evaluate(ws_url, expression, timeout=1.5):
|
|
|
170
184
|
except Exception:
|
|
171
185
|
pass
|
|
172
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
|
+
|
|
173
195
|
STATE_EXPRESSION = (
|
|
174
196
|
"({ url: location.href, title: document.title, "
|
|
175
197
|
"text: (document.body && document.body.innerText || '').slice(0, 20000), "
|
|
@@ -179,6 +201,46 @@ GEOMETRY_EXPRESSION = (
|
|
|
179
201
|
"({ browserWindow: { x: window.screenX, y: window.screenY, width: window.outerWidth, height: window.outerHeight }, "
|
|
180
202
|
"viewport: { width: window.innerWidth, height: window.innerHeight, deviceScaleFactor: window.devicePixelRatio } })"
|
|
181
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
|
|
182
244
|
|
|
183
245
|
def main():
|
|
184
246
|
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
|
|
@@ -197,6 +259,29 @@ def main():
|
|
|
197
259
|
print(json.dumps({"unavailable": "no http page among %d CDP targets on 127.0.0.1:%d" % (len(pages), port)}))
|
|
198
260
|
return
|
|
199
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
|
|
200
285
|
if mode == "geometry":
|
|
201
286
|
result = evaluate(ws_url, GEOMETRY_EXPRESSION) if ws_url else None
|
|
202
287
|
if not isinstance(result, dict):
|
|
@@ -233,6 +318,8 @@ export function chromeCdpProbeCommand(args) {
|
|
|
233
318
|
payload.targetId = args.targetId;
|
|
234
319
|
if (args.prefer !== undefined)
|
|
235
320
|
payload.prefer = args.prefer;
|
|
321
|
+
if (args.emulation !== undefined)
|
|
322
|
+
payload.emulation = args.emulation;
|
|
236
323
|
return `python3 -c ${shellSingleQuote(CHROME_CDP_PROBE_PY)} ${shellSingleQuote(JSON.stringify(payload))}`;
|
|
237
324
|
}
|
|
238
325
|
/**
|
|
@@ -254,8 +341,12 @@ export function parseChromeCdpProbeOutput(stdout) {
|
|
|
254
341
|
if (!parsed || typeof parsed !== "object")
|
|
255
342
|
return { unavailable: "probe output was not an object" };
|
|
256
343
|
const record = parsed;
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
}
|
|
259
350
|
const numberOr = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
260
351
|
const box = (value, keys) => {
|
|
261
352
|
if (!value || typeof value !== "object")
|
|
@@ -274,7 +365,23 @@ export function parseChromeCdpProbeOutput(stdout) {
|
|
|
274
365
|
const viewport = box(record.viewport, ["width", "height", "deviceScaleFactor"]);
|
|
275
366
|
const cdpPort = numberOr(record.cdpPort);
|
|
276
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
|
+
})();
|
|
277
382
|
return {
|
|
383
|
+
...(applied === undefined ? {} : { applied }),
|
|
384
|
+
...(fidelity === undefined ? {} : { fidelity }),
|
|
278
385
|
...(cdpPort === undefined ? {} : { cdpPort }),
|
|
279
386
|
...(typeof record.url === "string" && record.url.length > 0 ? { url: record.url } : {}),
|
|
280
387
|
...(typeof record.title === "string" && record.title.length > 0 ? { title: record.title } : {}),
|
|
@@ -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;AAwEjE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoR5C,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
|
@@ -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";
|
|
@@ -700,6 +701,18 @@ export declare function makeChromeBrowserStateObserver(desktop: E2BDesktopSandbo
|
|
|
700
701
|
export declare function makeChromeDesktopGeometryObserver(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, targetId?: string, onUnavailable?: (reason: string) => void): () => Promise<(Pick<RunDesktopGeometry, "browserWindow" | "viewport"> & {
|
|
701
702
|
targetId?: string;
|
|
702
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
|
+
}>;
|
|
703
716
|
/** Shared hosted-browser geometry capture used by per-lane and sequential shared-world routes. */
|
|
704
717
|
export declare function captureDesktopBrowserGeometry(args: {
|
|
705
718
|
desktop: E2BDesktopSandbox;
|