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 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/`):** 29 of 30 planted
57
- defects over two benchmark runs on an app we wrote, none invented on the clean arm; on two apps we
58
- did not write, 16 of 18 distinct findings confirmed against the source and 0 invented, across 14
59
- participants; and a modal that stopped 2 of 3 keyboard-first participants, was reported by
60
- all 3, and was never mentioned by 3 mouse-driving newcomers. Each file states
61
- its own caveats.
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 issue` | Print a public-safe GitHub issue draft without API mutation. |
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. There is no touch input, the
467
- device-pixel-ratio isn't rendered, and the user-agent stays desktop on this route; true
468
- touch/DPR/UA emulation arrives with the deterministic CDP actor. Device is run-wide
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
- /** "state": url/title/text/scrollY. "geometry": outer window + CSS viewport. "port": resolution only. */
22
- mode: "state" | "geometry" | "port";
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
@@ -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 evaluate(ws_url, expression, timeout=1.5):
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 = 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:
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
- message = b""
160
- if isinstance(reply, dict) and reply.get("id") == 1:
161
- result = reply.get("result") or {}
162
- inner = result.get("result") if isinstance(result, dict) else None
163
- return inner.get("value") if isinstance(inner, dict) else 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
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
- if (typeof record.unavailable === "string")
258
- return { unavailable: record.unavailable };
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;AAwCjE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+L5C,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,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,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ;QAAE,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;IACvF,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,OAAO;QACL,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"}
1
+ {"version":3,"file":"chrome-cdp-probe.js","sourceRoot":"","sources":["../src/chrome-cdp-probe.ts"],"names":[],"mappings":"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"}
@@ -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;