@proagentstore/cli 0.4.79 → 0.4.81
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.
|
@@ -56,14 +56,28 @@ const RESEARCH_TOOLS = [
|
|
|
56
56
|
},
|
|
57
57
|
];
|
|
58
58
|
/** Runs inside the page, through the browser's own evaluate tool — privileged, never offered to the CLI. */
|
|
59
|
-
const INSPECT_PAGE = `() => {
|
|
59
|
+
export const INSPECT_PAGE = `() => {
|
|
60
60
|
const text = (document.body ? document.body.innerText : "").slice(0, 20000).toLowerCase();
|
|
61
61
|
const has = (s) => !!document.querySelector(s);
|
|
62
62
|
const captcha = has('iframe[src*="hcaptcha.com"], .h-captcha, iframe[src*="challenges.cloudflare.com"], .cf-turnstile, iframe[src*="arkoselabs"], .geetest_holder')
|
|
63
63
|
|| /confirm (that )?you('?re| are) not a robot|i'?m not a robot|verify (that )?you('?re| are) (a )?human|checking (if the site connection is secure|your browser)/.test(text);
|
|
64
64
|
const login = has('input[type=password]');
|
|
65
65
|
const paywall = /subscribe to (continue|read)|you('ve| have) reached your (free )?(article )?limit|sign in to (continue|read)/.test(text);
|
|
66
|
-
|
|
66
|
+
// A bot check or an access-control page (#947). Read off the title, or off a SHORT body: these
|
|
67
|
+
// pages say little else, and a long article that merely contains "access denied" is not one.
|
|
68
|
+
const title = document.title.toLowerCase();
|
|
69
|
+
const blockedText = /access (is )?denied|403 forbidden|you don'?t have permission to access|(your )?request (was |has been )?blocked|unusual traffic from your (computer|network)|automated (queries|requests|access)|are you a (robot|human)|bot (detection|protection)|pardon our interruption|attention required/;
|
|
70
|
+
const accessBlocked = blockedText.test(title) || (text.length < 3000 && blockedText.test(text));
|
|
71
|
+
// A write affordance (#947): somewhere a run would pay, upload, or submit an application or an
|
|
72
|
+
// account. Search boxes and lone inputs are not; the fields are counted per form, search excluded.
|
|
73
|
+
const field = 'input:not([type=hidden]):not([type=search]):not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]), textarea, select';
|
|
74
|
+
const payOrUpload = has('input[autocomplete^="cc-"], input[type=file]');
|
|
75
|
+
const submitForm = Array.from(document.querySelectorAll("form")).some((f) => {
|
|
76
|
+
if (f.getAttribute("role") === "search" || f.querySelector("input[type=search]")) return false;
|
|
77
|
+
const send = Array.from(f.querySelectorAll('button, input[type=submit]')).some((b) => /submit|apply|send|pay|place order|checkout|register|sign up|create account/i.test((b.textContent || b.getAttribute("value") || "").trim()));
|
|
78
|
+
return send && f.querySelectorAll(field).length >= 2;
|
|
79
|
+
});
|
|
80
|
+
return { url: location.href, title: document.title.slice(0, 200), captcha, login, paywall, accessBlocked, writeForm: payOrUpload || submitForm };
|
|
67
81
|
}`;
|
|
68
82
|
/** A bare lowercase hostname, or null — the same rule the API applies to the lists it sends. */
|
|
69
83
|
export function hostOf(raw) {
|
|
@@ -95,6 +109,11 @@ export function evaluateResult(raw) {
|
|
|
95
109
|
return null;
|
|
96
110
|
}
|
|
97
111
|
}
|
|
112
|
+
const BLOCKER_WORDS = {
|
|
113
|
+
captcha: { noun: "a captcha", failure: "captcha" },
|
|
114
|
+
login_required: { noun: "a sign-in", failure: "login_required" },
|
|
115
|
+
access_blocked: { noun: "a bot check or access block", failure: "access_denied" },
|
|
116
|
+
};
|
|
98
117
|
/** The role and accessible name of a snapshot line holding this ref, e.g. `- link "Next" [ref=e12]`. */
|
|
99
118
|
export function refRole(snapshot, ref) {
|
|
100
119
|
for (const line of snapshot.split("\n")) {
|
|
@@ -115,6 +134,8 @@ export class BrowserBridge {
|
|
|
115
134
|
findings = [];
|
|
116
135
|
sourceFailures = [];
|
|
117
136
|
summary = null;
|
|
137
|
+
/** Tool calls that reached the bridge. Zero at exit means the CLI never used the browser (#944). */
|
|
138
|
+
calls = 0;
|
|
118
139
|
constructor(browser, host) {
|
|
119
140
|
this.browser = browser;
|
|
120
141
|
this.host = host;
|
|
@@ -128,6 +149,7 @@ export class BrowserBridge {
|
|
|
128
149
|
];
|
|
129
150
|
}
|
|
130
151
|
async callTool(name, args = {}) {
|
|
152
|
+
this.calls++;
|
|
131
153
|
if (name === "record_finding")
|
|
132
154
|
return this.recordFinding(args);
|
|
133
155
|
if (name === "report_source_failure")
|
|
@@ -249,17 +271,32 @@ export class BrowserBridge {
|
|
|
249
271
|
this.pages++;
|
|
250
272
|
this.noteNavigation(state);
|
|
251
273
|
const host = hostOf(state.url) ?? "";
|
|
252
|
-
|
|
274
|
+
// Pauses, never obstacles (#947): a captcha, a sign-in, a bot check or an access-control page is
|
|
275
|
+
// for a PERSON in the browser on this machine — the run waits, and re-checks once resumed.
|
|
276
|
+
const blocker = state.captcha ? "captcha" : state.login ? "login_required" : state.accessBlocked ? "access_blocked" : null;
|
|
253
277
|
if (blocker) {
|
|
278
|
+
const what = BLOCKER_WORDS[blocker];
|
|
254
279
|
this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: blocker } });
|
|
255
280
|
const outcome = await this.host.pause(blocker, { domain: host });
|
|
256
281
|
if (outcome !== "resumed")
|
|
257
|
-
return text(`${host} needs a person (${
|
|
282
|
+
return text(`${host} needs a person (${what.noun}) and nobody resolved it. Record it with report_source_failure (${what.failure}) and move on.`, true);
|
|
258
283
|
const after = await this.inspect();
|
|
259
|
-
if (after && (after.captcha || after.login))
|
|
260
|
-
return text(`${host} still shows ${
|
|
284
|
+
if (after && (after.captcha || after.login || after.accessBlocked))
|
|
285
|
+
return text(`${host} still shows ${what.noun}. Record it with report_source_failure (${what.failure}) and move on.`, true);
|
|
261
286
|
return text(`Resumed after the owner handled ${host}. Take a browser_snapshot to read the page.`);
|
|
262
287
|
}
|
|
288
|
+
// A page that pays, uploads, or submits an application or account is outside research. The
|
|
289
|
+
// tools to act on it do not exist here anyway; the pause is so the OWNER decides whether the run
|
|
290
|
+
// should be reading it at all, rather than the model wandering through a form it cannot fill.
|
|
291
|
+
if (state.writeForm) {
|
|
292
|
+
this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: "write_affordance" } });
|
|
293
|
+
const outcome = await this.host.pause("write_affordance", { domain: host });
|
|
294
|
+
if (outcome !== "resumed") {
|
|
295
|
+
await this.browser.callTool("browser_navigate_back", {}).catch(() => undefined);
|
|
296
|
+
return text(`${state.url} is a page for submitting, paying or uploading, and the owner did not ask the run to read it. Went back; do not return to it.`, true);
|
|
297
|
+
}
|
|
298
|
+
return text("The owner let the run read this page. It is still research only: nothing here can be filled, submitted or uploaded. Take a browser_snapshot to read it.");
|
|
299
|
+
}
|
|
263
300
|
if (state.paywall) {
|
|
264
301
|
this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: "paywall" } });
|
|
265
302
|
return text(`${host} is behind a paywall. Do not try to get around it: record it with report_source_failure (paywall) and move on.`, true);
|
|
@@ -278,7 +315,7 @@ export class BrowserBridge {
|
|
|
278
315
|
const v = res && !res.isError ? evaluateResult(textOf(res)) : null;
|
|
279
316
|
if (!v || typeof v.url !== "string")
|
|
280
317
|
return null;
|
|
281
|
-
return { url: v.url, title: typeof v.title === "string" ? v.title : "", captcha: !!v.captcha, login: !!v.login, paywall: !!v.paywall };
|
|
318
|
+
return { url: v.url, title: typeof v.title === "string" ? v.title : "", captcha: !!v.captcha, login: !!v.login, paywall: !!v.paywall, accessBlocked: !!v.accessBlocked, writeForm: !!v.writeForm };
|
|
282
319
|
}
|
|
283
320
|
recordFinding(args) {
|
|
284
321
|
const title = typeof args.title === "string" ? args.title.trim() : "";
|
|
@@ -143,6 +143,17 @@ export function signInHelp(engine) {
|
|
|
143
143
|
? "The Claude Code CLI on this machine is not signed in. Run `claude`, then /login, on that machine and start the run again."
|
|
144
144
|
: "The Codex CLI on this machine is not signed in. Run `codex login` on that machine and start the run again.";
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Why a run that never touched the browser bridge failed, and the step that fixes it (#944). The usual
|
|
148
|
+
* cause is the CLI refusing the bridge's tools (an approval policy, #952) or not loading the server.
|
|
149
|
+
*/
|
|
150
|
+
export function bridgeUnused(engine, said) {
|
|
151
|
+
const blocked = /approval|not (allowed|permitted)|permission|denied/i.test(said);
|
|
152
|
+
const cause = blocked
|
|
153
|
+
? `The ${engine === "claude" ? "Claude Code" : "Codex"} CLI was refused the PAGS browser tools.`
|
|
154
|
+
: `The ${engine === "claude" ? "Claude Code" : "Codex"} CLI finished without using the PAGS browser tools.`;
|
|
155
|
+
return `${cause} No page was opened, so this run found nothing. Update the CLI on that machine (npm i -g @proagentstore/cli), restart \`pags up\`, and start the run again.`;
|
|
156
|
+
}
|
|
146
157
|
/** The CLI's own closing text, from its structured output — the summary when it never called finish_research. */
|
|
147
158
|
export function finalText(engine, lines) {
|
|
148
159
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -27,7 +27,7 @@ import { RunnerInputError } from "../errors.js";
|
|
|
27
27
|
import { BrowserBridge, domainWithin } from "./bridge.js";
|
|
28
28
|
import { BRIDGE_ENV } from "./bridge-stdio.js";
|
|
29
29
|
import { LOCAL_BROWSER_AUTH_MODES, LOCAL_BROWSER_ENGINES, LOCAL_BROWSER_TASK_TYPE, redactDetail, redactText, secretEnvValues, } from "./contract.js";
|
|
30
|
-
import { buildEngineSpec, finalText, missingLogin, observedEngineAuth, researchPrompt, signInHelp } from "./engine.js";
|
|
30
|
+
import { bridgeUnused, buildEngineSpec, finalText, missingLogin, observedEngineAuth, researchPrompt, signInHelp } from "./engine.js";
|
|
31
31
|
const MAX_EVENTS = 2000;
|
|
32
32
|
const OUTPUT_LINES = 400;
|
|
33
33
|
const SAFE_ID = /^[A-Za-z0-9_.:-]{1,100}$/;
|
|
@@ -387,6 +387,11 @@ export class LocalBrowserRuntime {
|
|
|
387
387
|
const tail = run.output.slice(-5).join("\n").slice(-800);
|
|
388
388
|
return { outcome: "failed", summary, error: `The ${e.engine} CLI exited with code ${code}${tail ? `: ${tail}` : ""}` };
|
|
389
389
|
}
|
|
390
|
+
// A CLI that exits cleanly having never called the bridge did no research, whatever its prose
|
|
391
|
+
// says. Live run aac758dc (#952) ended `completed` with "the browser bridge required approval":
|
|
392
|
+
// no page opened, and the run looked like a success with nothing found.
|
|
393
|
+
if (!bridge?.calls)
|
|
394
|
+
return { outcome: "failed", summary, error: bridgeUnused(e.engine, summary) };
|
|
390
395
|
return { outcome: "completed", summary };
|
|
391
396
|
}
|
|
392
397
|
end(run, r) {
|