@proagentstore/cli 0.3.2 → 0.3.4
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.
|
@@ -194,3 +194,69 @@ export async function performBrowserAction(page, action, clickRobustly, resumeFi
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* After a WRITE action (type/select/check), read back what the field actually
|
|
199
|
+
* holds now + any validation error rendered near it. This is the semantic
|
|
200
|
+
* feedback the brain otherwise lacks: `fill()` succeeding doesn't mean the value
|
|
201
|
+
* "took" (masked/intl widgets concatenate; validators reject a format). Returns a
|
|
202
|
+
* short human string for the action log, or "" when there's nothing notable.
|
|
203
|
+
* Never throws.
|
|
204
|
+
*/
|
|
205
|
+
export async function inspectField(page, action) {
|
|
206
|
+
if (action.action !== "type" && action.action !== "select" && action.action !== "check")
|
|
207
|
+
return "";
|
|
208
|
+
if (!action.name)
|
|
209
|
+
return "";
|
|
210
|
+
try {
|
|
211
|
+
const role = action.role;
|
|
212
|
+
let loc = role ? page.getByRole(role, { name: action.name }) : page.getByText(action.name, { exact: false });
|
|
213
|
+
loc = typeof action.nth === "number" ? loc.nth(action.nth) : loc.first();
|
|
214
|
+
const el = await loc.elementHandle({ timeout: 1_500 }).catch(() => null);
|
|
215
|
+
if (!el)
|
|
216
|
+
return "";
|
|
217
|
+
const info = await el
|
|
218
|
+
.evaluate((node) => {
|
|
219
|
+
const e = node;
|
|
220
|
+
const raw = typeof e.value === "string" ? e.value : (e.getAttribute("aria-checked") ?? e.textContent ?? "");
|
|
221
|
+
const value = String(raw).trim().slice(0, 120);
|
|
222
|
+
const invalid = e.getAttribute("aria-invalid") === "true" || (typeof e.matches === "function" && e.matches(":invalid"));
|
|
223
|
+
let err = "";
|
|
224
|
+
const rx = /invalid|required|must|valid|error|format|enter a|please/i;
|
|
225
|
+
const describedby = e.getAttribute("aria-describedby");
|
|
226
|
+
if (describedby) {
|
|
227
|
+
for (const id of describedby.split(/\s+/)) {
|
|
228
|
+
const d = document.getElementById(id);
|
|
229
|
+
const t = (d?.textContent || "").trim();
|
|
230
|
+
if (t && rx.test(t)) {
|
|
231
|
+
err = t;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!err) {
|
|
237
|
+
const scope = e.closest("[class*=field], [class*=form-group], [class*=form-item], fieldset") || e.parentElement;
|
|
238
|
+
const cand = scope?.querySelector('[role="alert"], [class*=error i], [class*=invalid i], [class*=danger i], [class*=help-block i]');
|
|
239
|
+
const t = (cand?.textContent || "").trim();
|
|
240
|
+
if (t && rx.test(t) && t.length < 180)
|
|
241
|
+
err = t;
|
|
242
|
+
}
|
|
243
|
+
return { value, invalid, err: err.replace(/\s+/g, " ").slice(0, 180) };
|
|
244
|
+
})
|
|
245
|
+
.catch(() => null);
|
|
246
|
+
if (!info)
|
|
247
|
+
return "";
|
|
248
|
+
const typed = String(action.text ?? "").trim();
|
|
249
|
+
const parts = [];
|
|
250
|
+
if (info.err)
|
|
251
|
+
parts.push(`⚠ "${action.name}" REJECTED: "${info.err}"`);
|
|
252
|
+
if (info.value && (info.invalid || info.err || (typed && info.value !== typed))) {
|
|
253
|
+
parts.push(`"${action.name}" now reads "${info.value}"${typed && info.value !== typed ? ` (you sent "${typed}")` : ""}`);
|
|
254
|
+
}
|
|
255
|
+
if (info.invalid && !info.err)
|
|
256
|
+
parts.push(`"${action.name}" is marked invalid`);
|
|
257
|
+
return parts.join("; ");
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return "";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -55,6 +55,17 @@ Endpoints:
|
|
|
55
55
|
process.exit(0);
|
|
56
56
|
}
|
|
57
57
|
const config = configFromArgs();
|
|
58
|
+
// SECURITY: the runner's endpoints spawn CLIs and drive a browser as the user.
|
|
59
|
+
// Without a token, `authorize()` allows every request, so a tokenless runner must
|
|
60
|
+
// never be reachable off the local machine. Refuse to start if bound to a
|
|
61
|
+
// non-loopback address without a token. (`pags up` always generates one; this
|
|
62
|
+
// only guards the low-level binary run manually with --host 0.0.0.0 and no token.)
|
|
63
|
+
const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
64
|
+
if (!config.token && !LOOPBACK.has(config.host)) {
|
|
65
|
+
process.stderr.write(`Refusing to start: --host ${config.host} exposes the runner off-machine with no --token. ` +
|
|
66
|
+
`Set --token (or PAGS_RUNNER_TOKEN), or bind to 127.0.0.1.\n`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
58
69
|
const started = await startRunnerServer(config);
|
|
59
70
|
process.stdout.write(`FAGS browser runtime listening at ${started.url}\n`);
|
|
60
71
|
process.stdout.write(`Data dir: ${config.dataDir}\n`);
|
|
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { captureScreenshotDataUrl, challengeSolved, detectHumanChallenge } from "./challenge.js";
|
|
7
7
|
import { resolveRealChromeProfileDir, seedProfileCopy } from "./browser-profile.js";
|
|
8
|
-
import { performBrowserAction } from "./browser-actions.js";
|
|
8
|
+
import { inspectField, performBrowserAction } from "./browser-actions.js";
|
|
9
9
|
import { HumanHandoffError, RunnerInputError } from "./errors.js";
|
|
10
10
|
import { RunnerStore } from "./store.js";
|
|
11
11
|
import { CodingRuntime } from "./coding/runtime.js";
|
|
@@ -664,11 +664,15 @@ export class LocalRunner {
|
|
|
664
664
|
if (action.action === "click" || action.action === "navigate" || action.action === "key") {
|
|
665
665
|
await active.waitForLoadState("networkidle", { timeout: 3_500 }).catch(() => undefined);
|
|
666
666
|
}
|
|
667
|
+
// Read back what a write action actually did (real field value + any validation
|
|
668
|
+
// error) so the brain gets semantic feedback, not just "Playwright didn't throw".
|
|
669
|
+
const feedback = await inspectField(active, action).catch(() => "");
|
|
667
670
|
return {
|
|
668
671
|
ok: true,
|
|
669
672
|
url: active.url(),
|
|
670
673
|
title: await active.title().catch(() => ""),
|
|
671
674
|
challenge: await detectHumanChallenge(active),
|
|
675
|
+
feedback: feedback || undefined,
|
|
672
676
|
};
|
|
673
677
|
}
|
|
674
678
|
/**
|
package/dist/index.js
CHANGED
|
@@ -824,7 +824,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
824
824
|
}
|
|
825
825
|
}
|
|
826
826
|
for (const id of instanceIds) {
|
|
827
|
-
|
|
827
|
+
const mintToken = () => requestPags("POST", `/v1/relay/${apiPathSegment(id)}/token`, { ...opts, pagsToken }, {}).then((r) => r.token);
|
|
828
|
+
openRelaySocket(id, apiBase, mintToken, localUrl, runnerToken, force);
|
|
828
829
|
}
|
|
829
830
|
writeLine("Runtime registered with PAGS \u2713");
|
|
830
831
|
writeLine("");
|
|
@@ -844,11 +845,24 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
844
845
|
};
|
|
845
846
|
heartbeat();
|
|
846
847
|
}
|
|
847
|
-
function openRelaySocket(instanceId, wsBase,
|
|
848
|
+
function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false) {
|
|
848
849
|
let backoffMs = 1e3;
|
|
849
850
|
let reconnecting = false;
|
|
850
|
-
const connect = () => {
|
|
851
|
-
|
|
851
|
+
const connect = async () => {
|
|
852
|
+
let relayToken;
|
|
853
|
+
try {
|
|
854
|
+
relayToken = await mintToken();
|
|
855
|
+
} catch (e) {
|
|
856
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
857
|
+
const hint = /401|token|sign/i.test(msg) ? " (run `pags login`)" : "";
|
|
858
|
+
writeLine(`Relay token mint failed: ${instanceId.slice(0, 8)}\u2026${hint} \u2014 retrying in ${Math.round(backoffMs / 1e3)}s`);
|
|
859
|
+
setTimeout(() => {
|
|
860
|
+
connect();
|
|
861
|
+
}, backoffMs);
|
|
862
|
+
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const url = `${wsBase}/v1/relay/${encodeURIComponent(instanceId)}/connect?token=${encodeURIComponent(relayToken)}${force ? "&force=1" : ""}`;
|
|
852
866
|
const ws = new WebSocket(url);
|
|
853
867
|
ws.onopen = () => {
|
|
854
868
|
backoffMs = 1e3;
|