@proagentstore/cli 0.4.145 → 0.4.147
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/dist/browser-runner/challenge.js +9 -8
- package/dist/browser-runner/local-apply/bridge.js +11 -5
- package/dist/browser-runner/local-apply/follow-tab.js +32 -0
- package/dist/browser-runner/local-browser/replay.js +23 -0
- package/dist/browser-runner/runner.js +7 -10
- package/dist/browser-runner/test-chromium.js +29 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maskedPageScreenshot } from "./local-browser/replay.js";
|
|
1
2
|
/**
|
|
2
3
|
* Detect an anti-bot human challenge on the page (reCAPTCHA, hCaptcha,
|
|
3
4
|
* Cloudflare Turnstile, generic captcha). These can't be solved by the model,
|
|
@@ -74,13 +75,13 @@ export async function challengeSolved(page) {
|
|
|
74
75
|
})
|
|
75
76
|
.catch(() => false);
|
|
76
77
|
}
|
|
77
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Capture a downscaled JPEG screenshot as a data URL for the human-takeover UI — the STILL stored with a
|
|
80
|
+
* handoff. Form controls are blurred first and no still is kept when that cannot be confirmed (#1067): a
|
|
81
|
+
* handoff can be raised mid-form, and the person acts through the live takeover view (which is not
|
|
82
|
+
* blurred — it is the control surface), never through this preview.
|
|
83
|
+
*/
|
|
78
84
|
export async function captureScreenshotDataUrl(page) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return `data:image/jpeg;base64,${buf.toString("base64")}`;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
85
|
+
const shot = await maskedPageScreenshot(page, { quality: 55, timeout: 8_000 });
|
|
86
|
+
return shot ? `data:image/jpeg;base64,${shot}` : undefined;
|
|
86
87
|
}
|
|
@@ -31,6 +31,7 @@ import { ELEMENT_PROBE_FN } from "../commit-guard.js";
|
|
|
31
31
|
import { normalize } from "../local-artifact/engine.js";
|
|
32
32
|
import { evaluateResult, hostOf, refRole } from "../local-browser/bridge.js";
|
|
33
33
|
import { looksLikeSignIn, siteKeyOf } from "./site-session.js";
|
|
34
|
+
import { followOpenedTab, tabCount } from "./follow-tab.js";
|
|
34
35
|
const READ_TOOLS = new Set(["browser_navigate", "browser_navigate_back", "browser_snapshot", "browser_wait_for"]);
|
|
35
36
|
const FILL_TOOLS = new Set(["browser_type", "browser_select_option"]);
|
|
36
37
|
const BRIDGE_TOOLS = ["upload_artifact", "request_answer", "ready_for_review", "supervisor_checkpoint"];
|
|
@@ -424,10 +425,7 @@ export class ApplyBridge {
|
|
|
424
425
|
if (NAV_ROLES.has(t.role) || (t.role === "button" && PAGINATION_NAME.test(t.name.trim()))) {
|
|
425
426
|
if (this.pages >= this.host.limits.maxPages)
|
|
426
427
|
return this.refuse("browser_click", "read", `The run's limit of ${this.host.limits.maxPages} pages is reached. Call ready_for_review now.`);
|
|
427
|
-
|
|
428
|
-
if (res.isError)
|
|
429
|
-
return { content: [{ type: "text", text: textOf(res) }], isError: true };
|
|
430
|
-
return this.afterMove("browser_click", textOf(res));
|
|
428
|
+
return this.clickAndFollow(forward);
|
|
431
429
|
}
|
|
432
430
|
// A native file input opens the file chooser for upload_artifact — a DOM fact, whatever its label.
|
|
433
431
|
const fileInput = facts.tag === "input" && facts.type === "file";
|
|
@@ -643,10 +641,18 @@ export class ApplyBridge {
|
|
|
643
641
|
if (this.pages >= this.host.limits.maxPages)
|
|
644
642
|
return this.refuse("browser_click", "read", `The run's limit of ${this.host.limits.maxPages} pages is reached. Call ready_for_review now.`);
|
|
645
643
|
this.host.emit({ type: "policy.decision", detail: { tool: "browser_click", class: "entry", decision: "allowed", reason, role: t.role } });
|
|
644
|
+
return this.clickAndFollow(forward);
|
|
645
|
+
}
|
|
646
|
+
/** A page-changing click — followed into a tab it opened (#1133, `follow-tab.ts`). */
|
|
647
|
+
async clickAndFollow(forward) {
|
|
648
|
+
const before = await tabCount(this.browser);
|
|
646
649
|
const res = await this.browser.callTool("browser_click", forward);
|
|
647
650
|
if (res.isError)
|
|
648
651
|
return { content: [{ type: "text", text: textOf(res) }], isError: true };
|
|
649
|
-
|
|
652
|
+
if (!(await followOpenedTab(this.browser, before)))
|
|
653
|
+
return this.afterMove("browser_click", textOf(res));
|
|
654
|
+
this.host.emit({ type: "policy.decision", detail: { tool: "browser_click", class: "read", decision: "followed", reason: "opened_new_tab" } });
|
|
655
|
+
return this.afterMove("browser_click", `That click opened a new tab; the run now works in it. Take a browser_snapshot to read it.\n${textOf(res)}`);
|
|
650
656
|
}
|
|
651
657
|
/** The final Submit. Never under fill_and_review; once, gated and traced, under auto_submit. */
|
|
652
658
|
async submit(forward, why = "terminal_label") {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tab a click OPENED (#1133).
|
|
3
|
+
*
|
|
4
|
+
* SEEK's "Apply" can open its sign-in in a NEW tab. Playwright MCP adds a popup to its tab list but keeps
|
|
5
|
+
* the current tab where it was (`_onPageCreated` sets `_currentTab` only when there is none), and the CLI
|
|
6
|
+
* may not switch tabs (`browser_tabs` is refused to it). So every page check after that click — landing,
|
|
7
|
+
* checkpoint, ready_for_review, all of #1100's sign-in detection — read the job ad the click started on,
|
|
8
|
+
* saw no sign-in, and the run ended `review.ready` count 0 (f63b2963, on runner 0.4.145, after #1100).
|
|
9
|
+
*
|
|
10
|
+
* The bridge therefore counts tabs around a page-changing click and selects one that appeared DURING it,
|
|
11
|
+
* so its checks read the page the click led to. Only a new tab: the profile is the owner's real browser,
|
|
12
|
+
* and a tab already open is theirs.
|
|
13
|
+
*/
|
|
14
|
+
export async function tabCount(browser) {
|
|
15
|
+
const res = await browser.callTool("browser_tabs", { action: "list" }).catch(() => null);
|
|
16
|
+
if (!res || res.isError)
|
|
17
|
+
return null;
|
|
18
|
+
const body = (res.content ?? []).map((c) => c.text ?? "").join("\n");
|
|
19
|
+
// Only the `### Result` section: Playwright MCP may repeat the same list under `### Open tabs`.
|
|
20
|
+
const result = /### Result\n([\s\S]*?)(?=\n### |$)/.exec(body)?.[1] ?? body;
|
|
21
|
+
return (result.match(/^- \d+:/gm) ?? []).length;
|
|
22
|
+
}
|
|
23
|
+
/** Select the newest tab when the count grew across a click; true when the run now works in it. */
|
|
24
|
+
export async function followOpenedTab(browser, before) {
|
|
25
|
+
if (before === null)
|
|
26
|
+
return false;
|
|
27
|
+
const after = await tabCount(browser);
|
|
28
|
+
if (after === null || after <= before)
|
|
29
|
+
return false;
|
|
30
|
+
const selected = await browser.callTool("browser_tabs", { action: "select", index: after - 1 }).catch(() => null);
|
|
31
|
+
return !!selected && !selected.isError;
|
|
32
|
+
}
|
|
@@ -19,6 +19,29 @@ export const MASK_FORM_CONTROLS = `() => {
|
|
|
19
19
|
return document.getElementById(${JSON.stringify(MASK_ID)}) ? "masked" : "unmasked";
|
|
20
20
|
}`;
|
|
21
21
|
export const UNMASK_FORM_CONTROLS = `() => { const s = document.getElementById(${JSON.stringify(MASK_ID)}); if (s) s.remove(); return "unmasked"; }`;
|
|
22
|
+
/**
|
|
23
|
+
* The same form-control policy for a Playwright `Page` (#1067) — the legacy workflow browser tasks
|
|
24
|
+
* (`runtime_task`: JOB_APPLY / BROWSER_TASK), whose per-action frames were taken with nothing blurred,
|
|
25
|
+
* so a step that had just typed a phone number stored it in plain pixels. Blur, capture, unblur — and
|
|
26
|
+
* NO frame (undefined) when the blur cannot be confirmed, exactly as {@link ShotRecorder.capture}. The
|
|
27
|
+
* unblur runs even when the capture throws, so a failed frame never leaves the page blurred.
|
|
28
|
+
*/
|
|
29
|
+
export async function maskedPageScreenshot(page, opts) {
|
|
30
|
+
try {
|
|
31
|
+
if ((await page.evaluate(`(${MASK_FORM_CONTROLS})()`)) !== "masked")
|
|
32
|
+
return undefined;
|
|
33
|
+
try {
|
|
34
|
+
const buf = await page.screenshot({ type: "jpeg", quality: opts.quality, scale: "css", timeout: opts.timeout });
|
|
35
|
+
return buf.toString("base64");
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await page.evaluate(`(${UNMASK_FORM_CONTROLS})()`).catch(() => undefined);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
22
45
|
const textOf = (r) => (r.content ?? []).map((c) => c.text ?? "").join("\n");
|
|
23
46
|
export class ShotRecorder {
|
|
24
47
|
cap;
|
|
@@ -16,6 +16,7 @@ import { RunnerStore } from "./store.js";
|
|
|
16
16
|
import { CodingRuntime } from "./coding/runtime.js";
|
|
17
17
|
import { LocalBrowserRuntime } from "./local-browser/runtime.js";
|
|
18
18
|
import { LOCAL_BROWSER_TASK_TYPE } from "./local-browser/contract.js";
|
|
19
|
+
import { maskedPageScreenshot } from "./local-browser/replay.js";
|
|
19
20
|
import { LocalArtifactRuntime } from "./local-artifact/runtime.js";
|
|
20
21
|
import { LOCAL_ARTIFACT_TASK_TYPE } from "./local-artifact/contract.js";
|
|
21
22
|
import { LocalApplyRuntime } from "./local-apply/runtime.js";
|
|
@@ -908,17 +909,13 @@ export class LocalRunner {
|
|
|
908
909
|
return head.slice(0, 400);
|
|
909
910
|
}
|
|
910
911
|
/** A small JPEG screenshot (base64, no data: prefix) of the page — one per action,
|
|
911
|
-
* so the cloud can store it per step and the whole run can be replayed visually.
|
|
912
|
+
* so the cloud can store it per step and the whole run can be replayed visually.
|
|
913
|
+
* Form controls are BLURRED first, and no frame is kept when that cannot be confirmed (#1067) —
|
|
914
|
+
* the Scout/Runner replay policy (#1063), which this legacy path used to skip.
|
|
915
|
+
* scale:"css" avoids 2× retina bloat; a short timeout means a stuck page never delays the
|
|
916
|
+
* action (the screenshot is best-effort, not the point). */
|
|
912
917
|
async shot(page) {
|
|
913
|
-
|
|
914
|
-
// scale:"css" avoids 2× retina bloat; a short timeout means a stuck page
|
|
915
|
-
// never delays the action (the screenshot is best-effort, not the point).
|
|
916
|
-
const buf = await page.screenshot({ type: "jpeg", quality: 45, scale: "css", timeout: 8_000 });
|
|
917
|
-
return buf.toString("base64");
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
return undefined;
|
|
921
|
-
}
|
|
918
|
+
return maskedPageScreenshot(page, { quality: 45, timeout: 8_000 });
|
|
922
919
|
}
|
|
923
920
|
/**
|
|
924
921
|
* After a WRITE action, read the field's REAL value back via the standard
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch Chromium for a test that runs real DOM — after making sure Chromium is installed.
|
|
3
|
+
*
|
|
4
|
+
* In CI the browsers are installed AFTER the unit + integration step (ci.yml). The binary those tests
|
|
5
|
+
* use arrives mid-run, because `LocalRunner` installs it on first launch (`ensureChromiumInstalled`,
|
|
6
|
+
* `runner-browser.test.ts`). A file that called `chromium.launch()` on its own passed only if vitest
|
|
7
|
+
* happened to schedule it after that download finished. `masked-page-screenshot.test.ts` (#1067) lost
|
|
8
|
+
* that race on every CI run from 98b25b98: "Executable doesn't exist … chrome-headless-shell", 1.7s
|
|
9
|
+
* before the download completed. `inspect-page.test.ts` won it by scheduling luck.
|
|
10
|
+
*
|
|
11
|
+
* So this runs the same idempotent `playwright install chromium` the runner runs. It is a quick check
|
|
12
|
+
* when the binary is present, and Playwright's own install lock makes a concurrent install wait rather
|
|
13
|
+
* than collide. `PAGS_SKIP_PLAYWRIGHT_INSTALL=1` skips it, as it does for the runner. Test-only.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { chromium } from "playwright";
|
|
19
|
+
let installed = false;
|
|
20
|
+
export async function launchTestChromium() {
|
|
21
|
+
if (!installed && process.env.PAGS_SKIP_PLAYWRIGHT_INSTALL !== "1") {
|
|
22
|
+
const cli = join(dirname(createRequire(import.meta.url).resolve("playwright")), "cli.js");
|
|
23
|
+
const r = spawnSync(process.execPath, [cli, "install", "chromium"], { stdio: "inherit", env: process.env });
|
|
24
|
+
if (r.status !== 0)
|
|
25
|
+
throw new Error("`playwright install chromium` failed — real-DOM tests cannot run without it.");
|
|
26
|
+
installed = true;
|
|
27
|
+
}
|
|
28
|
+
return chromium.launch();
|
|
29
|
+
}
|