@proagentstore/cli 0.4.96 → 0.4.97

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.
@@ -95,12 +95,12 @@ export const LOCAL_APPLY_BLOCK_REASONS = [
95
95
  * dispatch, naming the update, rather than silently producing the older behaviour (`apply.ts`
96
96
  * `runnerContractProblem`).
97
97
  *
98
- * ── Why it moved to 0.4.96 (#1013)
98
+ * ── Why it moved to 0.4.97 (#1013)
99
99
  *
100
- * #953 adds the read-only `/local-apply/preflight` route used before an email lead can enter
101
- * tailoring. That route lives in `packages/browser-runner`, which ships INSIDE the published CLI.
102
- * A machine below this floor cannot make the live/expired/unverifiable observation, so it must
103
- * not silently look equivalent to a current runner.
100
+ * #1013 adds the exact isolated-run browser handoff used when a sign-in or CAPTCHA blocks an
101
+ * application. That route lives in `packages/browser-runner`, which ships INSIDE the published
102
+ * CLI. A machine below this floor cannot retain the actual isolated page and must not silently
103
+ * substitute the shared/default browser page.
104
104
  *
105
105
  * A behaviour change in the runner is a CONTRACT change, because the cloud's decisions assume it.
106
106
  * So this floor moves with it: #1013's exact-page login/CAPTCHA handoff and post-submit receipt
@@ -111,7 +111,7 @@ export const LOCAL_APPLY_BLOCK_REASONS = [
111
111
  * been published. `cliAtLeast` treats an unreported version as capable, which is the convention
112
112
  * every MIN_CLI gate here follows.
113
113
  */
114
- export const LOCAL_APPLY_CONTRACT_MIN_CLI = "0.4.96";
114
+ export const LOCAL_APPLY_CONTRACT_MIN_CLI = "0.4.97";
115
115
  export const LOCAL_APPLY_DIAGNOSTIC_CAUSES = ["bridge_unused", "engine_exited_nonzero", "timed_out", "no_engine_output", "submit_unconfirmed"];
116
116
  export const LOCAL_APPLY_CONFIRMATION_MARKERS = ["page_text", "url_receipt", "already_applied_notice"];
117
117
  export const LOCAL_APPLY_SIGNALS = [
@@ -0,0 +1,87 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { McpRuntime } from "../mcp-runtime.js";
4
+ /** Bind application handoff to the run browser, not to the runner-wide active page. */
5
+ export function createLocalApplyBrowserFactory(options) {
6
+ return async (profile, runDir) => {
7
+ if (profile === "isolated")
8
+ return createIsolatedApplyBrowser(runDir, options.headless);
9
+ const browser = await options.defaultBrowser(profile, runDir);
10
+ return { ...browser, handoffPage: options.sharedPage };
11
+ };
12
+ }
13
+ /**
14
+ * Start the disposable browser context for one application run and retain only its current live
15
+ * page for the bounded takeover adapter. This deliberately does not seed, copy, or persist an
16
+ * owner profile: the profile remains the run's existing throwaway `runDir/profile` directory.
17
+ */
18
+ export async function createIsolatedApplyBrowser(runDir, headless) {
19
+ const playwright = await import("playwright");
20
+ const profileDir = join(runDir, "profile");
21
+ let context;
22
+ let mcp;
23
+ let activePage = null;
24
+ let stopped = false;
25
+ try {
26
+ context = await playwright.chromium.launchPersistentContext(profileDir, {
27
+ headless,
28
+ acceptDownloads: true,
29
+ viewport: null,
30
+ args: [
31
+ "--disable-blink-features=AutomationControlled",
32
+ "--start-maximized",
33
+ "--window-size=1512,982",
34
+ "--remote-debugging-port=0",
35
+ "--use-fake-ui-for-media-stream",
36
+ "--use-fake-device-for-media-stream",
37
+ ],
38
+ });
39
+ await context.addInitScript(() => {
40
+ Object.defineProperty(navigator, "webdriver", { get: () => undefined });
41
+ }).catch(() => undefined);
42
+ const track = (page) => {
43
+ activePage = page;
44
+ // Never fall back to another tab after the exact page closes: that would hand a
45
+ // person a different site/page. A lost page is terminal for this handoff.
46
+ page.once("close", () => {
47
+ if (activePage === page)
48
+ activePage = null;
49
+ });
50
+ };
51
+ context.on("page", track);
52
+ const initial = context.pages().at(-1) ?? await context.newPage();
53
+ track(initial);
54
+ mcp = new McpRuntime();
55
+ await mcp.start({ cdpEndpoint: await cdpEndpoint(profileDir) });
56
+ return {
57
+ tools: mcp,
58
+ handoffPage: () => activePage && !activePage.isClosed() ? activePage : null,
59
+ stop: async () => {
60
+ if (stopped)
61
+ return;
62
+ stopped = true;
63
+ activePage = null;
64
+ await mcp.stop().catch(() => undefined);
65
+ await context.close().catch(() => undefined);
66
+ },
67
+ };
68
+ }
69
+ catch (error) {
70
+ await mcp?.stop().catch(() => undefined);
71
+ await context?.close().catch(() => undefined);
72
+ throw error;
73
+ }
74
+ }
75
+ async function cdpEndpoint(profileDir) {
76
+ const portFile = join(profileDir, "DevToolsActivePort");
77
+ for (let i = 0; i < 50; i++) {
78
+ if (existsSync(portFile)) {
79
+ const text = await import("node:fs/promises").then(({ readFile }) => readFile(portFile, "utf8"));
80
+ const port = text.split("\n", 1)[0]?.trim();
81
+ if (port)
82
+ return `http://127.0.0.1:${port}`;
83
+ }
84
+ await new Promise((resolve) => setTimeout(resolve, 100));
85
+ }
86
+ throw new Error("could not read the local application browser CDP port");
87
+ }
@@ -17,8 +17,11 @@ export class LocalApplyHandoffRuntime {
17
17
  throw new RunnerInputError("A handoff already exists for this application run", 409);
18
18
  if (!this.host.takeover)
19
19
  return this.closed(run, request.handoffId, "unavailable");
20
+ const page = run.browser?.handoffPage?.();
21
+ if (!page || page.isClosed())
22
+ return this.closed(run, request.handoffId, "page_lost");
20
23
  try {
21
- await this.host.takeover.open(request);
24
+ await this.host.takeover.open(request, page);
22
25
  }
23
26
  catch {
24
27
  return this.closed(run, request.handoffId, "profile_unavailable");
@@ -5,14 +5,12 @@ export function isLocalApplyTakeover(id) { return id.startsWith(prefix); }
5
5
  export function createLocalApplyTakeoverAdapter(deps) {
6
6
  const key = (handoffId) => `${prefix}${handoffId}`;
7
7
  return {
8
- async open(request) {
9
- if (request.browserProfile !== "default")
10
- throw new RunnerInputError("This browser profile has no live handoff page", 409);
8
+ async open(request, page) {
11
9
  const id = key(request.handoffId);
12
10
  if (deps.get(id))
13
11
  throw new RunnerInputError("A local application handoff already exists", 409);
14
- // Do not create a page here: a destroyed page must remain a lost handoff, never look ready.
15
- const page = deps.activePage();
12
+ // Do not create or substitute a page here. The runtime supplied the page retained by
13
+ // this exact run/profile; a destroyed page must remain a lost handoff, never look ready.
16
14
  if (!page || page.isClosed())
17
15
  throw new RunnerInputError("The local application page is no longer available", 409);
18
16
  deps.set(id, page);
@@ -18,6 +18,7 @@ import { LocalArtifactRuntime } from "./local-artifact/runtime.js";
18
18
  import { LOCAL_ARTIFACT_TASK_TYPE } from "./local-artifact/contract.js";
19
19
  import { LocalApplyRuntime } from "./local-apply/runtime.js";
20
20
  import { LOCAL_APPLY_TASK_TYPE } from "./local-apply/contract.js";
21
+ import { createLocalApplyBrowserFactory } from "./local-apply/live-run-browser.js";
21
22
  import { createLocalApplyTakeoverAdapter, isLocalApplyTakeover } from "./local-apply/takeover-adapter.js";
22
23
  import { WORKFLOW_DRIVEN_TASKS } from "./task-types.js";
23
24
  /** True for a plain object. */
@@ -104,13 +105,14 @@ export class LocalRunner {
104
105
  await mcp.start({ userDataDir: join(runDir, "profile"), headless: config.headless });
105
106
  return { tools: mcp, stop: () => mcp.stop() };
106
107
  };
108
+ const applyBrowserFor = createLocalApplyBrowserFactory({ headless: config.headless, defaultBrowser: browserFor, sharedPage: () => this.activePage });
107
109
  this.localBrowser = new LocalBrowserRuntime({ dataDir: config.dataDir, selfUrl: () => this.selfUrl, browserFor });
108
110
  this.localArtifact = new LocalArtifactRuntime({ dataDir: config.dataDir });
109
111
  this.localApply = new LocalApplyRuntime({
110
112
  dataDir: config.dataDir,
111
113
  selfUrl: () => this.selfUrl,
112
- browserFor,
113
- takeover: createLocalApplyTakeoverAdapter({ activePage: () => this.activePage, 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) }),
114
+ browserFor: applyBrowserFor,
115
+ takeover: 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) }),
114
116
  });
115
117
  // Tasks paused/running on a previous process are orphaned now — their
116
118
  // pages and takeover sessions are gone. Fail them so the board is clean.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.96",
3
+ "version": "0.4.97",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",