@proagentstore/cli 0.4.80 → 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
- return { url: location.href, title: document.title.slice(0, 200), captcha, login, paywall };
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")) {
@@ -252,17 +271,32 @@ export class BrowserBridge {
252
271
  this.pages++;
253
272
  this.noteNavigation(state);
254
273
  const host = hostOf(state.url) ?? "";
255
- const blocker = state.captcha ? "captcha" : state.login ? "login_required" : null;
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;
256
277
  if (blocker) {
278
+ const what = BLOCKER_WORDS[blocker];
257
279
  this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: blocker } });
258
280
  const outcome = await this.host.pause(blocker, { domain: host });
259
281
  if (outcome !== "resumed")
260
- return text(`${host} needs a person (${blocker === "captcha" ? "a captcha" : "a sign-in"}) and nobody resolved it. Record it with report_source_failure (${blocker}) and move on.`, true);
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);
261
283
  const after = await this.inspect();
262
- if (after && (after.captcha || after.login))
263
- return text(`${host} still shows ${blocker === "captcha" ? "a captcha" : "a sign-in"}. Record it with report_source_failure (${blocker}) and move on.`, true);
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);
264
286
  return text(`Resumed after the owner handled ${host}. Take a browser_snapshot to read the page.`);
265
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
+ }
266
300
  if (state.paywall) {
267
301
  this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: "paywall" } });
268
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);
@@ -281,7 +315,7 @@ export class BrowserBridge {
281
315
  const v = res && !res.isError ? evaluateResult(textOf(res)) : null;
282
316
  if (!v || typeof v.url !== "string")
283
317
  return null;
284
- 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 };
285
319
  }
286
320
  recordFinding(args) {
287
321
  const title = typeof args.title === "string" ? args.title.trim() : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.80",
3
+ "version": "0.4.81",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",