@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.
- package/dist/browser-runner/local-apply/contract.js +6 -6
- package/dist/browser-runner/local-apply/live-run-browser.js +87 -0
- package/dist/browser-runner/local-apply/runtime-handoff.js +4 -1
- package/dist/browser-runner/local-apply/takeover-adapter.js +3 -5
- package/dist/browser-runner/runner.js +4 -2
- package/package.json +1 -1
|
@@ -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.
|
|
98
|
+
* ── Why it moved to 0.4.97 (#1013)
|
|
99
99
|
*
|
|
100
|
-
* #
|
|
101
|
-
*
|
|
102
|
-
* A machine below this floor cannot
|
|
103
|
-
*
|
|
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.
|
|
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
|
|
15
|
-
|
|
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({
|
|
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.
|