@proagentstore/cli 0.4.130 → 0.4.131

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.
@@ -30,6 +30,7 @@ import { CONFIRMATION_INTERVAL_SECONDS, CONFIRMATION_LOOKS, CONFIRMATION_TEXT_RE
30
30
  import { ELEMENT_PROBE_FN } from "../commit-guard.js";
31
31
  import { normalize } from "../local-artifact/engine.js";
32
32
  import { evaluateResult, hostOf, refRole } from "../local-browser/bridge.js";
33
+ import { looksLikeSignIn, siteKeyOf } from "./site-session.js";
33
34
  const READ_TOOLS = new Set(["browser_navigate", "browser_navigate_back", "browser_snapshot", "browser_wait_for"]);
34
35
  const FILL_TOOLS = new Set(["browser_type", "browser_select_option"]);
35
36
  const BRIDGE_TOOLS = ["upload_artifact", "request_answer", "ready_for_review", "supervisor_checkpoint"];
@@ -314,10 +315,16 @@ export class ApplyBridge {
314
315
  if (!blocker)
315
316
  return text(toolText);
316
317
  this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: blocker } });
318
+ const site = blocker === "login_required" && host ? siteKeyOf(host) : null;
319
+ if (site)
320
+ await this.host.sessions?.beginSignIn(site).catch(() => undefined);
317
321
  const ok = await this.pauseFor({ reason: blocker, url: state.url, domain: host }, async () => {
318
322
  const after = await this.inspect();
319
323
  return !!after && this.blockerOf(after) === null;
320
324
  });
325
+ // A sign-in the owner completed here is remembered for this browser profile (#1094).
326
+ if (ok && site)
327
+ await this.recordSignIn(site);
321
328
  if (!ok)
322
329
  return text(`${host} needs a person (${blocker.replace(/_/g, " ")}) and it was not resolved. Do not work around it. Stop now.`, true);
323
330
  return text("Resumed after the owner handled it. Take a browser_snapshot to read the page.");
@@ -664,6 +671,9 @@ export class ApplyBridge {
664
671
  const domain = hostOf(before.url) ?? "";
665
672
  if (!this.host.isAllowed(domain))
666
673
  return this.refuse("browser_click", "submit", `${domain} is not one of this application's sites; nothing was submitted.`, domain);
674
+ const gate = await this.sessionGate(before.url, domain);
675
+ if (gate)
676
+ return gate;
667
677
  this.submitAttempted = true;
668
678
  this.host.emit({ type: "submit.attempted", url: before.url, domain, detail: { class: "submit", gateId: this.host.gateId } });
669
679
  const res = await this.browser.callTool("browser_click", forward);
@@ -681,7 +691,19 @@ export class ApplyBridge {
681
691
  this.host.emit({ type: "submit.confirmed", url: at.url, domain: hostOf(at.url) ?? domain, detail: { gateId: this.host.gateId, marker: seen.marker, ...evidence } });
682
692
  return text("Submitted, and the site confirmed it. Stop now.");
683
693
  }
684
- this.blocked = { reason: "submit_unconfirmed", questions: ["The application was submitted but the site did not confirm it. Check the employer's site or your email before anything is retried."] };
694
+ // Landed on a sign-in page (#1094): the outcome stays `submit_unconfirmed` (#1013 never infers
695
+ // "not sent" from a redirect), but this profile now knows the site needs a sign-in, so the next
696
+ // run asks the owner to sign in BEFORE pressing instead of bouncing again.
697
+ const wall = after && looksLikeSignIn(after.url, after.login) ? this.host.sessions?.learnSignInWall(before.url, after.url) : null;
698
+ if (wall)
699
+ this.host.emit({ type: "policy.decision", domain, detail: { tool: "browser_click", class: "session", decision: "learned", reason: "bounced_to_sign_in", site: wall } });
700
+ this.blocked = {
701
+ reason: "submit_unconfirmed",
702
+ questions: [
703
+ "The application was submitted but the site did not confirm it. Check the employer's site or your email before anything is retried." +
704
+ (wall ? ` The site asked for a sign-in, so the next run on this machine will ask you to sign in to ${wall} first.` : ""),
705
+ ],
706
+ };
685
707
  this.host.emit({ type: "submit.unconfirmed", ...(after ? { url: after.url } : {}), domain, detail: { gateId: this.host.gateId, ...evidence } });
686
708
  return text("The submit was pressed but no confirmation was seen. Do not press it again. Stop now.", true);
687
709
  }
@@ -705,6 +727,43 @@ export class ApplyBridge {
705
727
  }
706
728
  return { after, looks: CONFIRMATION_LOOKS };
707
729
  }
730
+ /**
731
+ * Before an uncertain press (#1094): is this browser profile signed in to the site? If the ledger
732
+ * says it is not, or its session cookies are gone, the run pauses `login_required` so the owner
733
+ * signs in ONCE in this same browser (the existing handoff) — and nothing has been pressed, so
734
+ * there is no submit to be uncertain about. Returns null when the press may go ahead.
735
+ */
736
+ async sessionGate(url, domain) {
737
+ const ledger = this.host.sessions;
738
+ if (!ledger)
739
+ return null;
740
+ const check = await ledger.check(url).catch(() => null);
741
+ if (!check || check.state === "ok")
742
+ return null;
743
+ const site = check.site;
744
+ this.host.emit({ type: "policy.decision", domain, detail: { tool: "browser_click", class: "session", decision: "paused", reason: `session_${check.reason}`, site } });
745
+ await ledger.beginSignIn(site).catch(() => undefined);
746
+ const ok = await this.pauseFor({
747
+ reason: "login_required",
748
+ url,
749
+ domain,
750
+ question: `Sign in to ${site} in this browser. It is remembered on this machine, so later applications reuse it. Nothing has been submitted.`,
751
+ });
752
+ if (!ok) {
753
+ this.blocked = { reason: "login_required", questions: [`Sign in to ${site} on this machine's application browser, then retry. Nothing was submitted.`] };
754
+ return this.refuse("browser_click", "submit", `${site} needs a sign-in in this browser and the owner did not complete one. Nothing was submitted. Stop now.`, domain);
755
+ }
756
+ await this.recordSignIn(site);
757
+ return text(`The owner signed in to ${site}; this browser remembers it. Nothing has been submitted. Open the job page again with browser_navigate (${url.split(/[?#]/, 1)[0]}), take a browser_snapshot, and continue from there.`);
758
+ }
759
+ async recordSignIn(site) {
760
+ const ledger = this.host.sessions;
761
+ if (!ledger)
762
+ return;
763
+ const result = await ledger.completeSignIn(site).catch(() => null);
764
+ if (result)
765
+ this.host.emit({ type: "policy.decision", detail: { tool: "sign_in", class: "session", decision: "recorded", site, count: result.fingerprint, basis: result.verifiable ? "cookies" : "unverifiable" } });
766
+ }
708
767
  /** A cloud-persisted checkpoint, not a prompt instruction, authorizes each page's writes. */
709
768
  requireSupervisor(tool, actionClass) {
710
769
  if (this.supervisorApproved)
@@ -179,7 +179,20 @@ const isHttpUrl = (v) => /^https?:\/\/[^\s/]+/i.test(v);
179
179
  // from `LOCAL_APPLY_CONFIRMATION_MARKERS`, how many times the page was read, and whether it moved.
180
180
  // Each is an id, a count or a boolean — the same whitelist rule as everything else here, so no page
181
181
  // text and no typed value can ride along on a `submit.confirmed` / `submit.unconfirmed` event.
182
- const DETAIL_KEYS = new Set(["engine", "authMode", "engineAuth", "mode", "class", "tool", "decision", "reason", "rule", "role", "kind", "path", "sha256", "bytes", "gateId", "exitCode", "count", "status", "basis", "source", "checkpointId", "directive", "phase", "actions", "filled", "uploaded", "marker", "looks", "urlChanged", "titleChanged", "shot", "point"]);
182
+ const DETAIL_KEYS = new Set(["engine", "authMode", "engineAuth", "mode", "class", "tool", "decision", "reason", "rule", "role", "kind", "path", "sha256", "bytes", "gateId", "exitCode", "count", "status", "basis", "source", "checkpointId", "directive", "phase", "actions", "filled", "uploaded", "marker", "looks", "urlChanged", "titleChanged", "shot", "point", "site"]);
183
+ /**
184
+ * A page URL as a trace may keep it (#1069): origin and path, no query or fragment — a sign-in page's
185
+ * carry OAuth state, PKCE values and session ids. "" when it is not an http(s) URL.
186
+ */
187
+ export function traceUrl(raw) {
188
+ try {
189
+ const u = new URL(raw);
190
+ return u.protocol === "http:" || u.protocol === "https:" ? `${u.origin}${u.pathname}` : "";
191
+ }
192
+ catch {
193
+ return "";
194
+ }
195
+ }
183
196
  /** A validated event, or null. Detail keeps only whitelisted, primitive, bounded values. */
184
197
  export function parseLocalApplyEvent(raw) {
185
198
  if (!raw || typeof raw !== "object")
@@ -190,9 +203,15 @@ export function parseLocalApplyEvent(raw) {
190
203
  if (!type || !at || Number.isNaN(Date.parse(at)))
191
204
  return null;
192
205
  const event = { type, at };
206
+ // Origin and path only (#1069): a sign-in page's query and fragment carry OAuth state and session
207
+ // ids, and the trace is read back verbatim by `application_run` and the console. Same rule as the
208
+ // #1063 artifact captions, so an older runner that still sends the full URL is covered here too.
193
209
  const url = str(o.url, 2000);
194
- if (url && isHttpUrl(url))
195
- event.url = url;
210
+ if (url && isHttpUrl(url)) {
211
+ const kept = traceUrl(url);
212
+ if (kept)
213
+ event.url = kept;
214
+ }
196
215
  const domain = str(o.domain, 253);
197
216
  if (domain)
198
217
  event.domain = domain.toLowerCase();
@@ -1,18 +1,38 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { McpRuntime } from "../mcp-runtime.js";
4
+ import { SiteSessionLedger } from "./site-session.js";
5
+ /**
6
+ * Where an application profile lives on this machine (#1094) — documented in
7
+ * `APPLICATION_PROFILES.md` so the owner can find and inspect it:
8
+ *
9
+ * <dataDir>/application-profiles/<profile>/site-sessions.json which sites it is signed in to (no values)
10
+ * <dataDir>/application-profiles/isolated/browser/ the `isolated` profile's browser data
11
+ *
12
+ * The `default` profile's browser data is the runner's own browser (`<dataDir>/chrome-profile`, or
13
+ * the private real-profile copy), which already persisted; only its ledger lives here.
14
+ */
15
+ export function applicationProfileDir(dataDir, profile) {
16
+ return join(dataDir, "application-profiles", profile);
17
+ }
18
+ /**
19
+ * One run at a time per on-disk profile. Chrome locks a user-data-dir to one process, so a second
20
+ * launch on the same directory would fail obscurely; this says what is happening instead.
21
+ */
22
+ const inUse = new Set();
4
23
  /** Bind application handoff to the run browser, not to the runner-wide active page. */
5
24
  export function createLocalApplyBrowserFactory(options) {
6
25
  return async (profile, runDir) => {
26
+ const ledgerPath = join(applicationProfileDir(options.dataDir, profile), "site-sessions.json");
7
27
  if (profile === "isolated")
8
- return createIsolatedApplyBrowser(runDir, options.headless);
28
+ return createPersistentApplyBrowser(join(applicationProfileDir(options.dataDir, profile), "browser"), ledgerPath, options.headless);
9
29
  const browser = await options.defaultBrowser(profile, runDir);
10
- return { ...browser, handoffPage: options.sharedPage };
30
+ return { ...browser, handoffPage: options.sharedPage, sessions: new SiteSessionLedger(ledgerPath, options.defaultCookies) };
11
31
  };
12
32
  }
13
33
  /**
14
34
  * Keep local-apply's browser construction out of `runner.ts`: research uses the generic factory,
15
- * while application runs get a page-owning isolated context for the scoped handoff.
35
+ * while application runs get a page-owning persistent context for the scoped handoff.
16
36
  */
17
37
  export function createRunnerBrowserFactories(options) {
18
38
  const browserFor = async (profile, runDir) => {
@@ -22,21 +42,40 @@ export function createRunnerBrowserFactories(options) {
22
42
  await mcp.start({ userDataDir: join(runDir, "profile"), headless: options.headless });
23
43
  return { tools: mcp, stop: () => mcp.stop() };
24
44
  };
25
- return { browserFor, applyBrowserFor: createLocalApplyBrowserFactory({ headless: options.headless, defaultBrowser: browserFor, sharedPage: options.sharedPage }) };
45
+ return {
46
+ browserFor,
47
+ applyBrowserFor: createLocalApplyBrowserFactory({
48
+ headless: options.headless,
49
+ dataDir: options.dataDir,
50
+ defaultBrowser: browserFor,
51
+ defaultCookies: options.defaultCookies,
52
+ sharedPage: options.sharedPage,
53
+ }),
54
+ };
26
55
  }
27
56
  /**
28
- * Start the disposable browser context for one application run and retain only its current live
29
- * page for the bounded takeover adapter. This deliberately does not seed, copy, or persist an
30
- * owner profile: the profile remains the run's existing throwaway `runDir/profile` directory.
57
+ * Start the application browser on this machine's PERSISTENT `isolated` profile (#1094) and retain
58
+ * only its current live page for the bounded takeover adapter.
59
+ *
60
+ * Until #1094 this was the run's throwaway `runDir/profile`, deleted with the run, so a sign-in the
61
+ * owner completed in one run was gone by the next and every SEEK run bounced to its sign-in page.
62
+ * The profile is still never seeded from, copied to, or shared with the owner's everyday browser: it
63
+ * is a dedicated application browser that now remembers its own sign-ins, like a normal browser.
31
64
  */
32
- export async function createIsolatedApplyBrowser(runDir, headless) {
65
+ export async function createPersistentApplyBrowser(profileDir, ledgerPath, headless) {
66
+ if (inUse.has(profileDir))
67
+ throw new Error("This machine's application browser profile is in use by another run; try again when it ends.");
68
+ inUse.add(profileDir);
33
69
  const playwright = await import("playwright");
34
- const profileDir = join(runDir, "profile");
35
70
  let context;
36
71
  let mcp;
37
72
  let activePage = null;
38
73
  let stopped = false;
39
74
  try {
75
+ mkdirSync(profileDir, { recursive: true, mode: 0o700 });
76
+ // A persistent profile keeps the LAST launch's port file; read before Chrome rewrites it, the
77
+ // tools would attach to a dead port. Chrome writes a fresh one on every start.
78
+ rmSync(join(profileDir, "DevToolsActivePort"), { force: true });
40
79
  context = await playwright.chromium.launchPersistentContext(profileDir, {
41
80
  headless,
42
81
  acceptDownloads: true,
@@ -67,9 +106,11 @@ export async function createIsolatedApplyBrowser(runDir, headless) {
67
106
  track(initial);
68
107
  mcp = new McpRuntime();
69
108
  await mcp.start({ cdpEndpoint: await cdpEndpoint(profileDir) });
109
+ const ctx = context;
70
110
  return {
71
111
  tools: mcp,
72
112
  handoffPage: () => activePage && !activePage.isClosed() ? activePage : null,
113
+ sessions: new SiteSessionLedger(ledgerPath, async () => (await ctx.cookies()).map((c) => ({ name: c.name, domain: c.domain, expires: c.expires }))),
73
114
  stop: async () => {
74
115
  if (stopped)
75
116
  return;
@@ -77,12 +118,14 @@ export async function createIsolatedApplyBrowser(runDir, headless) {
77
118
  activePage = null;
78
119
  await mcp.stop().catch(() => undefined);
79
120
  await context.close().catch(() => undefined);
121
+ inUse.delete(profileDir);
80
122
  },
81
123
  };
82
124
  }
83
125
  catch (error) {
84
126
  await mcp?.stop().catch(() => undefined);
85
127
  await context?.close().catch(() => undefined);
128
+ inUse.delete(profileDir);
86
129
  throw error;
87
130
  }
88
131
  }
@@ -37,7 +37,7 @@ import { ApplyBridge } from "./bridge.js";
37
37
  import { APPLY_SHOT_CAP, ShotRecorder } from "../local-browser/replay.js";
38
38
  import { confirmationSignals } from "./confirmation.js";
39
39
  import { listingUnavailableReason } from "./unavailable.js";
40
- import { LOCAL_APPLY_AUTH_MODES, LOCAL_APPLY_CAPS, LOCAL_APPLY_ENGINES, LOCAL_APPLY_MODES, LOCAL_APPLY_TASK_TYPE, parseLocalApplyResult, } from "./contract.js";
40
+ import { LOCAL_APPLY_AUTH_MODES, LOCAL_APPLY_CAPS, LOCAL_APPLY_ENGINES, LOCAL_APPLY_MODES, LOCAL_APPLY_TASK_TYPE, parseLocalApplyResult, traceUrl, } from "./contract.js";
41
41
  import { applyPrompt, buildApplyEngineSpec, observedApplyAuth } from "./engine.js";
42
42
  import { LocalApplyHandoffRuntime } from "./runtime-handoff.js";
43
43
  const MAX_EVENTS = 1000;
@@ -397,6 +397,7 @@ export class LocalApplyRuntime {
397
397
  limits: e.limits,
398
398
  mode: e.policy.mode,
399
399
  gateId: e.policy.submitGate?.gateId,
400
+ sessions: run.browser.sessions,
400
401
  }, e.policy.screenshots === false ? undefined : run.shots);
401
402
  if (spec.mcpConfig)
402
403
  writeFileSync(spec.mcpConfig.path, spec.mcpConfig.json, { mode: 0o600 });
@@ -527,7 +528,10 @@ export class LocalApplyRuntime {
527
528
  emit(run, ev) {
528
529
  if (run.events.length >= MAX_EVENTS)
529
530
  return;
530
- run.events.push({ ...ev, at: new Date(this.now()).toISOString(), seq: ++run.seq });
531
+ // Origin and path only (#1069): a sign-in page's query carries OAuth state and session ids.
532
+ const url = ev.url ? traceUrl(ev.url) : "";
533
+ const { url: _raw, ...rest } = ev;
534
+ run.events.push({ ...rest, ...(url ? { url } : {}), at: new Date(this.now()).toISOString(), seq: ++run.seq });
531
535
  }
532
536
  pause(run, p) {
533
537
  if (run.state === "ended" || run.cancelled)
@@ -535,8 +539,10 @@ export class LocalApplyRuntime {
535
539
  if (run.state === "running") {
536
540
  run.activeMs += this.now() - run.activeSince;
537
541
  run.state = "paused";
538
- run.pause = p;
539
- this.emit(run, { type: "run.paused", pauseReason: p.reason, ...(p.url ? { url: p.url } : {}), ...(p.domain ? { domain: p.domain } : {}) });
542
+ const url = p.url ? traceUrl(p.url) : "";
543
+ const { url: _raw, ...rest } = p;
544
+ run.pause = { ...rest, ...(url ? { url } : {}) };
545
+ this.emit(run, { type: "run.paused", pauseReason: p.reason, ...(url ? { url } : {}), ...(p.domain ? { domain: p.domain } : {}) });
540
546
  }
541
547
  return new Promise((done) => run.waiters.push(done));
542
548
  }
@@ -0,0 +1,141 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ /**
4
+ * Sites whose one-click apply only works for a signed-in user. SEEK is listed from the verified
5
+ * failure in #1094 (two fill runs, two machines, both bounced to `login.seek.com`). A site not listed
6
+ * here joins the ledger the first time a press bounces to its sign-in page.
7
+ */
8
+ export const SIGN_IN_REQUIRED_SITES = ["seek.com", "seek.com.au", "seek.co.nz"];
9
+ const SECOND_LEVEL = new Set(["com", "co", "net", "org", "gov", "edu", "ac", "id"]);
10
+ /** `au.seek.com` → `seek.com`; `www.seek.com.au` → `seek.com.au`. Good enough for a site key, not a PSL. */
11
+ export function siteKeyOf(host) {
12
+ const labels = host.toLowerCase().replace(/\.$/, "").split(".").filter(Boolean);
13
+ if (labels.length <= 2)
14
+ return labels.join(".");
15
+ const tld = labels[labels.length - 1];
16
+ const sld = labels[labels.length - 2];
17
+ return labels.slice(tld.length === 2 && SECOND_LEVEL.has(sld) ? -3 : -2).join(".");
18
+ }
19
+ /** Does this page look like a sign-in page? A password field, or a login/SSO host or path. */
20
+ export function looksLikeSignIn(url, hasPassword) {
21
+ if (hasPassword)
22
+ return true;
23
+ try {
24
+ const u = new URL(url);
25
+ return /^(login|signin|sign-in|auth|accounts?|sso|id)\./i.test(u.hostname) || /\/(login|log-in|signin|sign-in|oauth2?|authorize)(\/|$)/i.test(u.pathname);
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function cookieSite(c) {
32
+ return siteKeyOf(c.domain.replace(/^\./, ""));
33
+ }
34
+ function live(c, nowMs) {
35
+ return c.expires === -1 || c.expires * 1000 > nowMs;
36
+ }
37
+ export class SiteSessionLedger {
38
+ path;
39
+ cookies;
40
+ now;
41
+ file;
42
+ /** Cookie names (with expiry) seen per site when a sign-in pause began, to diff on resume. */
43
+ before = new Map();
44
+ constructor(path, cookies, now = Date.now) {
45
+ this.path = path;
46
+ this.cookies = cookies;
47
+ this.now = now;
48
+ this.file = SiteSessionLedger.read(path);
49
+ }
50
+ static read(path) {
51
+ try {
52
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
53
+ if (parsed && parsed.version === 1 && parsed.sites && typeof parsed.sites === "object")
54
+ return parsed;
55
+ }
56
+ catch {
57
+ // Missing or unreadable: start empty. The worst case is one extra sign-in prompt.
58
+ }
59
+ return { version: 1, sites: {} };
60
+ }
61
+ entries() {
62
+ return Object.values(this.file.sites);
63
+ }
64
+ /** Is this profile signed in to the site that `url` is on, as far as can be shown? */
65
+ async check(url) {
66
+ const host = hostname(url);
67
+ if (!host)
68
+ return { state: "ok", site: null };
69
+ const site = siteKeyOf(host);
70
+ const entry = this.file.sites[site];
71
+ if (!entry)
72
+ return SIGN_IN_REQUIRED_SITES.includes(site) ? { state: "missing", site, reason: "never_signed_in" } : { state: "ok", site: null };
73
+ if (entry.state === "sign_in_needed")
74
+ return { state: "missing", site, reason: entry.basis === "bounced_to_sign_in" ? "bounced_to_sign_in" : "never_signed_in" };
75
+ if (!entry.verifiable || !entry.cookieNames?.length)
76
+ return { state: "ok", site };
77
+ const now = this.now();
78
+ const present = new Set((await this.cookies()).filter((c) => cookieSite(c) === site && live(c, now)).map((c) => c.name));
79
+ return entry.cookieNames.every((n) => present.has(n)) ? { state: "ok", site } : { state: "missing", site, reason: "expired" };
80
+ }
81
+ /** A sign-in pause is starting for `site`: remember which of its cookies exist now. */
82
+ async beginSignIn(site) {
83
+ const now = this.now();
84
+ const seen = new Map();
85
+ for (const c of await this.cookies())
86
+ if (cookieSite(c) === site && live(c, now))
87
+ seen.set(c.name, c.expires);
88
+ this.before.set(site, seen);
89
+ }
90
+ /** The owner resumed after signing in: record the cookies that appeared or were renewed. Names only. */
91
+ async completeSignIn(site) {
92
+ const before = this.before.get(site) ?? new Map();
93
+ this.before.delete(site);
94
+ const now = this.now();
95
+ const names = (await this.cookies())
96
+ .filter((c) => cookieSite(c) === site && live(c, now) && (!before.has(c.name) || before.get(c.name) !== c.expires))
97
+ .map((c) => c.name);
98
+ const cookieNames = [...new Set(names)].sort().slice(0, 50);
99
+ const at = new Date(now).toISOString();
100
+ const prior = this.file.sites[site];
101
+ this.file.sites[site] = {
102
+ site,
103
+ state: "signed_in",
104
+ basis: "owner_signed_in",
105
+ ...(prior?.loginHost ? { loginHost: prior.loginHost } : {}),
106
+ cookieNames,
107
+ verifiable: cookieNames.length > 0,
108
+ signedInAt: at,
109
+ updatedAt: at,
110
+ };
111
+ this.save();
112
+ return { fingerprint: cookieNames.length, verifiable: cookieNames.length > 0 };
113
+ }
114
+ /** A press on `fromUrl` landed on a sign-in page: the next run must ask for a sign-in first. */
115
+ learnSignInWall(fromUrl, signInUrl) {
116
+ const from = hostname(fromUrl);
117
+ if (!from)
118
+ return null;
119
+ const site = siteKeyOf(from);
120
+ const at = new Date(this.now()).toISOString();
121
+ const loginHost = hostname(signInUrl) ?? undefined;
122
+ this.file.sites[site] = { site, state: "sign_in_needed", basis: "bounced_to_sign_in", ...(loginHost ? { loginHost } : {}), updatedAt: at };
123
+ this.save();
124
+ return site;
125
+ }
126
+ save() {
127
+ mkdirSync(dirname(this.path), { recursive: true });
128
+ const tmp = `${this.path}.tmp`;
129
+ writeFileSync(tmp, `${JSON.stringify(this.file, null, 2)}\n`, { mode: 0o600 });
130
+ renameSync(tmp, this.path);
131
+ }
132
+ }
133
+ function hostname(url) {
134
+ try {
135
+ const u = new URL(url);
136
+ return u.protocol === "http:" || u.protocol === "https:" ? u.hostname.toLowerCase() : null;
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ }
@@ -104,7 +104,7 @@ export class LocalRunner {
104
104
  this.store = new RunnerStore(config.dataDir);
105
105
  this.crashLog = new CrashLog(join(config.dataDir, "crash-reports.json"), runtimeCrashIdentity());
106
106
  this.coding = new CodingRuntime(join(config.dataDir, "repos"));
107
- const { browserFor, applyBrowserFor } = createRunnerBrowserFactories({ headless: config.headless, defaultTools: () => this.getMcp(), sharedPage: () => this.activePage });
107
+ const { browserFor, applyBrowserFor } = createRunnerBrowserFactories({ headless: config.headless, dataDir: config.dataDir, defaultTools: () => this.getMcp(), defaultCookies: async () => (await (await this.getBrowserContext()).cookies()).map((c) => ({ name: c.name, domain: c.domain, expires: c.expires })), sharedPage: () => this.activePage });
108
108
  this.localBrowser = new LocalBrowserRuntime({ dataDir: config.dataDir, selfUrl: () => this.selfUrl, browserFor });
109
109
  this.localArtifact = new LocalArtifactRuntime({ dataDir: config.dataDir });
110
110
  const localApplyTakeover = createLocalApplyTakeoverAdapter({ get: (id) => this.takeovers.get(id), set: (id, page) => this.takeovers.set(id, { page, reason: "local_apply", humanDone: false }), frame: (id) => this.takeoverFrame(id), input: (id, input) => this.takeoverInput(id, input), end: (id) => this.endTakeover(id) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.130",
3
+ "version": "0.4.131",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",