@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
- openRelaySocket(id, apiBase, pagsToken, localUrl, runnerToken, force);
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, pagsToken, localUrl, runnerToken, force = false) {
848
+ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false) {
848
849
  let backoffMs = 1e3;
849
850
  let reconnecting = false;
850
- const connect = () => {
851
- const url = `${wsBase}/v1/relay/${encodeURIComponent(instanceId)}/connect?token=${encodeURIComponent(pagsToken)}${force ? "&force=1" : ""}`;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",