@proagentstore/cli 0.4.136 → 0.4.138

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.
@@ -61,7 +61,8 @@ const CONSENT_NAME = /\b(agree|consent|terms|privacy|acknowledge|certify|accept|
61
61
  * A field that is a credential: a sign-in, never an application answer. It includes the box an emailed or texted
62
62
  * one-time code goes into (#1014) — "Enter the 6-digit code we emailed you", "Sign-in code" — which has no password
63
63
  * field beside it, so the page check never sees a sign-in: the CLI is stopped at the field and the owner takes the
64
- * exact session over (#1013). The Runner holds no mailbox access and never reads, asks for or types a code itself.
64
+ * exact session over (#1013). The CLI never reads, asks for or types a code; on SEEK, PAGS may enter one into the
65
+ * paused browser itself (`email-code.ts`, #1106).
65
66
  * Ordinary "code" answers (a postcode, a promo or referral code) are not credentials.
66
67
  */
67
68
  export const CREDENTIAL_NAME = /password|passcode|one[- ]?time|\botp\b|2fa|two[- ]factor|\b(?:verification|security|sign[- ]?in|log[- ]?in|login|access|authentication|confirmation) code\b|\b\d[- ]?digit code\b|\bcode (?:that )?we (?:sent|emailed|texted)\b|\bcode (?:sent|emailed|texted) to\b/i;
@@ -37,6 +37,14 @@ export const LOCAL_APPLY_CANCEL_PATH = "/local-apply/cancel";
37
37
  export const LOCAL_APPLY_PREFLIGHT_PATH = "/local-apply/preflight";
38
38
  /** Delivers a cloud-persisted supervisory decision to one paused checkpoint. */
39
39
  export const LOCAL_APPLY_DIRECTIVE_PATH = "/local-apply/directive";
40
+ /**
41
+ * The email-code sign-in (#1106), on a run paused `login_required`: `{runId, step: "request", email}` has the runner
42
+ * enter the sign-in email and ask the site for a code; `{runId, step: "enter", code}` types the code PAGS read from
43
+ * the owner's Gmail and, once the site lets the browser in, releases the pause. The code is never stored or traced.
44
+ */
45
+ export const LOCAL_APPLY_EMAIL_CODE_PATH = "/local-apply/email-code";
46
+ /** Sites whose sign-in is a code emailed to the account address. SEEK's page was read live for #1106. */
47
+ export const EMAIL_CODE_SIGN_IN_SITES = ["seek.com", "seek.com.au", "seek.co.nz"];
40
48
  export const LOCAL_APPLY_ENGINES = ["claude", "codex"];
41
49
  export const LOCAL_APPLY_AUTH_MODES = ["machine", "subscription"];
42
50
  export const LOCAL_APPLY_MODES = ["fill_and_review", "auto_submit"];
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The email-code sign-in (#1106): a fill run paused `login_required` on SEEK signs itself in.
3
+ *
4
+ * SEEK signs a jobseeker in with a code emailed to the account address. The page was read live
5
+ * (2026-10-16): `au.seek.com/oauth/login?returnUrl=…` lands on `login.seek.com`, which offers
6
+ * `textbox "Email address"` and `button "Email me a sign in code"`. The runner does the page half:
7
+ * `request` enters the address the cloud names and asks for the code; `enter` types the code the
8
+ * cloud read from the owner's connected Gmail and waits for the site to let the browser in. The cloud
9
+ * does the mailbox half (`workers/api/src/lib/local-apply/email-code-sign-in.ts`); this machine never
10
+ * touches a mailbox.
11
+ *
12
+ * It drives the run's own browser while the run is paused, so the CLI — blocked in its pause — never
13
+ * sees the address or the code, and neither enters the trace: events carry the site and a reason.
14
+ * Anything the page does not offer (no email box, no code box, still on the sign-in page after the
15
+ * code) ends `failed`, and the pause stays for the owner: this is the default path, never the only one.
16
+ *
17
+ * Not verified live: the code-entry page (reaching it sends a real code to a real inbox). It is read
18
+ * by role and name — one code box, or one box per digit — the same way as the first page.
19
+ */
20
+ import { RunnerInputError } from "../errors.js";
21
+ import { evaluateResult } from "../local-browser/bridge.js";
22
+ import { EMAIL_CODE_SIGN_IN_SITES } from "./contract.js";
23
+ import { looksLikeSignIn, siteKeyOf } from "./site-session.js";
24
+ const EMAIL_FIELD = /e-?mail/i;
25
+ const SEND_BUTTON = /^(email|send) me an? (sign[- ]?in |log[- ]?in |verification )?code$|^send (an? )?(sign[- ]?in |verification )?code$/i;
26
+ const CODE_FIELD = /code|one[- ]?time|verification|otp|passcode|digit/i;
27
+ const CONFIRM_BUTTON = /^(verify|continue|sign in|log in|submit|confirm|next)\b/i;
28
+ const LOOKS = 6;
29
+ const LOOK_MS = 2000;
30
+ /** Every `- role "name" [ref=…]` line of a snapshot. */
31
+ export function controlsOf(snapshot) {
32
+ const out = [];
33
+ for (const line of snapshot.split("\n")) {
34
+ const m = line.match(/^\s*-\s*([a-z]+)(?:\s+"([^"]*)")?[^\n]*\[ref=([^\]\s]+)\]/i);
35
+ if (m)
36
+ out.push({ role: m[1].toLowerCase(), name: m[2] ?? "", ref: m[3] });
37
+ }
38
+ return out;
39
+ }
40
+ /** The box(es) a code goes in: one named for a code, or one box per digit (4–8 unnamed or "digit" boxes). */
41
+ export function codeBoxes(controls) {
42
+ const boxes = controls.filter((c) => (c.role === "textbox" || c.role === "spinbutton") && !EMAIL_FIELD.test(c.name));
43
+ const named = boxes.find((c) => CODE_FIELD.test(c.name) && !/digit\s*\d/i.test(c.name));
44
+ if (named)
45
+ return [named];
46
+ const perDigit = boxes.filter((c) => !c.name || /digit/i.test(c.name));
47
+ return perDigit.length >= 4 && perDigit.length <= 8 ? perDigit : [];
48
+ }
49
+ const textOf = (r) => (r.content ?? []).map((c) => c.text ?? "").join("\n");
50
+ async function snapshot(tools) {
51
+ return controlsOf(textOf(await tools.callTool("browser_snapshot", {}).catch(() => ({ content: [] }))));
52
+ }
53
+ async function pageUrl(tools) {
54
+ const res = await tools.callTool("browser_evaluate", { function: "() => location.href" }).catch(() => null);
55
+ const v = res && !res.isError ? evaluateResult(textOf(res)) : null;
56
+ return typeof v === "string" ? v : null;
57
+ }
58
+ /** Run one step on a run paused for a sign-in. Never throws for a page that is not as expected. */
59
+ export async function runEmailCodeStep(target, raw) {
60
+ const o = raw && typeof raw === "object" ? raw : {};
61
+ const wait = target.wait ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
62
+ const { tools } = target;
63
+ const url = await pageUrl(tools);
64
+ const host = url ? new URL(url).hostname : "";
65
+ const site = host ? siteKeyOf(host) : "";
66
+ const fail = (reason) => {
67
+ target.emit({ type: "policy.decision", ...(host ? { domain: host } : {}), detail: { tool: "email_code_sign_in", class: "session", decision: "failed", reason, ...(site ? { site } : {}) } });
68
+ return { state: "failed", reason };
69
+ };
70
+ if (!url || !EMAIL_CODE_SIGN_IN_SITES.includes(site))
71
+ return fail("unsupported_site");
72
+ if (o.step === "request") {
73
+ const email = typeof o.email === "string" ? o.email.trim() : "";
74
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || email.length > 254)
75
+ throw new RunnerInputError("request needs the sign-in email");
76
+ // A pause before the press (#1094) is on the job page: open SEEK's sign-in, returning to that page.
77
+ if (!looksLikeSignIn(url, false)) {
78
+ const u = new URL(url);
79
+ await tools.callTool("browser_navigate", { url: `https://${u.host}/oauth/login?returnUrl=${encodeURIComponent(u.pathname + u.search)}` });
80
+ await wait(LOOK_MS);
81
+ }
82
+ const first = await snapshot(tools);
83
+ const box = first.find((c) => c.role === "textbox" && EMAIL_FIELD.test(c.name));
84
+ if (!box)
85
+ return fail("no_email_field");
86
+ const send = first.find((c) => c.role === "button" && SEND_BUTTON.test(c.name.trim()));
87
+ if (!send)
88
+ return fail("no_send_button");
89
+ await tools.callTool("browser_type", { element: box.name, target: box.ref, text: email });
90
+ await tools.callTool("browser_click", { element: send.name, target: send.ref });
91
+ for (let i = 0; i < LOOKS; i++) {
92
+ await wait(LOOK_MS);
93
+ if (codeBoxes(await snapshot(tools)).length) {
94
+ target.emit({ type: "policy.decision", domain: host, detail: { tool: "email_code_sign_in", class: "session", decision: "code_requested", site } });
95
+ return { state: "code_requested" };
96
+ }
97
+ }
98
+ return fail("no_code_field");
99
+ }
100
+ if (o.step === "enter") {
101
+ const code = typeof o.code === "string" ? o.code.trim() : "";
102
+ if (!/^[A-Za-z0-9]{4,10}$/.test(code))
103
+ throw new RunnerInputError("enter needs the code");
104
+ const controls = await snapshot(tools);
105
+ const boxes = codeBoxes(controls);
106
+ if (!boxes.length || (boxes.length > 1 && boxes.length !== code.length))
107
+ return fail("no_code_field");
108
+ if (boxes.length === 1)
109
+ await tools.callTool("browser_type", { element: boxes[0].name || "code", target: boxes[0].ref, text: code });
110
+ else
111
+ for (const [i, b] of boxes.entries())
112
+ await tools.callTool("browser_type", { element: b.name || `digit ${i + 1}`, target: b.ref, text: code[i] });
113
+ // Some code pages sign in as the last digit lands; the rest have a button.
114
+ const confirm = controls.find((c) => c.role === "button" && CONFIRM_BUTTON.test(c.name.trim()) && !/resend|email me|send/i.test(c.name));
115
+ if (confirm)
116
+ await tools.callTool("browser_click", { element: confirm.name, target: confirm.ref }).catch(() => undefined);
117
+ for (let i = 0; i < LOOKS; i++) {
118
+ await wait(LOOK_MS);
119
+ const now = await pageUrl(tools);
120
+ if (now && !looksLikeSignIn(now, false) && !codeBoxes(await snapshot(tools)).length) {
121
+ target.emit({ type: "policy.decision", domain: new URL(now).hostname, detail: { tool: "email_code_sign_in", class: "session", decision: "signed_in", site } });
122
+ target.resume();
123
+ return { state: "signed_in" };
124
+ }
125
+ }
126
+ return fail("code_rejected");
127
+ }
128
+ throw new RunnerInputError("step must be request or enter");
129
+ }
@@ -34,6 +34,7 @@ import { redactText, secretEnvValues } from "../local-browser/contract.js";
34
34
  import { finalText, missingLogin, signInHelp } from "../local-browser/engine.js";
35
35
  import { resolveWorkspacePath } from "../local-browser/runtime.js";
36
36
  import { ApplyBridge } from "./bridge.js";
37
+ import { runEmailCodeStep } from "./email-code.js";
37
38
  import { APPLY_SHOT_CAP, ShotRecorder } from "../local-browser/replay.js";
38
39
  import { confirmationSignals } from "./confirmation.js";
39
40
  import { listingUnavailableReason } from "./unavailable.js";
@@ -225,7 +226,8 @@ export class LocalApplyRuntime {
225
226
  return { state: "unavailable", jobUrl: parsed.toString(), reason: gone };
226
227
  // An accessible Apply control is an active path, unlike a generic careers/search URL.
227
228
  if (/(?:button|link)\s+"[^"]{0,80}\bapply(?: now| for| here)?\b|\bapply (?:now|for (?:this )?(?:job|role|position))\b|\bstart application\b/i.test(page)) {
228
- return { state: "live", jobUrl: parsed.toString(), applyUrl: parsed.toString(), evidence: "apply_control_present" };
229
+ const title = page.match(/^\s*-?\s*Page Title:\s*(.+)$/m)?.[1]?.trim().slice(0, 300); // names a posting pasted with none (#1098)
230
+ return { state: "live", jobUrl: parsed.toString(), applyUrl: parsed.toString(), evidence: "apply_control_present", ...(title ? { title } : {}) };
229
231
  }
230
232
  return { state: "unverifiable", jobUrl: parsed.toString(), reason: "no_active_apply_path" };
231
233
  }
@@ -582,6 +584,13 @@ export class LocalApplyRuntime {
582
584
  for (const w of run.waiters.splice(0))
583
585
  w(outcome);
584
586
  }
587
+ /** The email-code sign-in (#1106) on a run paused for one: its own browser, its trace, and its release. */
588
+ emailCode(raw) {
589
+ const run = this.get(String(raw?.runId ?? ""));
590
+ if (run.state !== "paused" || run.pause?.reason !== "login_required" || !run.browser)
591
+ throw new RunnerInputError("The run is not paused for a sign-in", 409);
592
+ return runEmailCodeStep({ tools: run.browser.tools, emit: (ev) => this.emit(run, ev), resume: () => this.release(run, "resumed") }, raw);
593
+ }
585
594
  /** The owner acted: add their answers to what values may be grounded in, admit any site they allowed, release. */
586
595
  resume(raw) {
587
596
  const o = (raw && typeof raw === "object" ? raw : {});
@@ -280,6 +280,8 @@ async function route(runner, req, res) {
280
280
  if (req.method === "POST" && path === "/local-apply/resume") {
281
281
  return json(res, 200, runner.localApply.resume(await readJson(req)));
282
282
  }
283
+ if (req.method === "POST" && path === "/local-apply/email-code")
284
+ return json(res, 200, await runner.localApply.emailCode(await readJson(req)));
283
285
  if (req.method === "POST" && path === "/local-apply/directive") {
284
286
  return json(res, 200, runner.localApply.directive(await readJson(req)));
285
287
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.136",
3
+ "version": "0.4.138",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",