@proagentstore/cli 0.4.95 → 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 +20 -8
- package/dist/browser-runner/local-apply/handoff-routes.js +21 -0
- package/dist/browser-runner/local-apply/live-run-browser.js +87 -0
- package/dist/browser-runner/local-apply/runtime-handoff.js +120 -0
- package/dist/browser-runner/local-apply/runtime.js +13 -0
- package/dist/browser-runner/local-apply/takeover-adapter.js +26 -0
- package/dist/browser-runner/runner.js +10 -2
- package/dist/browser-runner/server.js +4 -1
- package/dist/index.js +4 -2
- package/package.json +1 -1
|
@@ -35,6 +35,17 @@ export const LOCAL_APPLY_CANCEL_PATH = "/local-apply/cancel";
|
|
|
35
35
|
export const LOCAL_APPLY_PREFLIGHT_PATH = "/local-apply/preflight";
|
|
36
36
|
/** Delivers a cloud-persisted supervisory decision to one paused checkpoint. */
|
|
37
37
|
export const LOCAL_APPLY_DIRECTIVE_PATH = "/local-apply/directive";
|
|
38
|
+
/**
|
|
39
|
+
* A short-lived, owner-authorized view of the *existing* browser page for one local-apply run.
|
|
40
|
+
* The id is opaque and carries no browser/profile material. It deliberately is not a task id:
|
|
41
|
+
* local-apply runs are not generic runner tasks.
|
|
42
|
+
*/
|
|
43
|
+
export const LOCAL_APPLY_HANDOFF_PATH = "/local-apply/handoff";
|
|
44
|
+
export const LOCAL_APPLY_HANDOFF_STATUS_PATH = "/local-apply/handoff-status";
|
|
45
|
+
export const LOCAL_APPLY_HANDOFF_FRAME_PATH = "/local-apply/handoff/frame";
|
|
46
|
+
export const LOCAL_APPLY_HANDOFF_INPUT_PATH = "/local-apply/handoff/input";
|
|
47
|
+
export const LOCAL_APPLY_HANDOFF_RESUME_PATH = "/local-apply/handoff/resume";
|
|
48
|
+
export const LOCAL_APPLY_HANDOFF_END_PATH = "/local-apply/handoff/end";
|
|
38
49
|
export const LOCAL_APPLY_ENGINES = ["claude", "codex"];
|
|
39
50
|
export const LOCAL_APPLY_AUTH_MODES = ["machine", "subscription"];
|
|
40
51
|
export const LOCAL_APPLY_MODES = ["fill_and_review", "auto_submit"];
|
|
@@ -84,23 +95,23 @@ export const LOCAL_APPLY_BLOCK_REASONS = [
|
|
|
84
95
|
* dispatch, naming the update, rather than silently producing the older behaviour (`apply.ts`
|
|
85
96
|
* `runnerContractProblem`).
|
|
86
97
|
*
|
|
87
|
-
* ── Why it moved to 0.4.
|
|
98
|
+
* ── Why it moved to 0.4.97 (#1013)
|
|
88
99
|
*
|
|
89
|
-
* #
|
|
90
|
-
*
|
|
91
|
-
* A machine below this floor cannot
|
|
92
|
-
*
|
|
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.
|
|
93
104
|
*
|
|
94
105
|
* A behaviour change in the runner is a CONTRACT change, because the cloud's decisions assume it.
|
|
95
|
-
* So this floor moves with it:
|
|
96
|
-
*
|
|
106
|
+
* So this floor moves with it: #1013's exact-page login/CAPTCHA handoff and post-submit receipt
|
|
107
|
+
* evidence are refused together rather than silently degrading to an unhelpful `submit_unconfirmed` state.
|
|
97
108
|
*
|
|
98
109
|
* Bump this when the runner's half of the contract changes again, together with the CLI version
|
|
99
110
|
* that ships it — `policy.test.ts` pins the pair, so the floor cannot name a release that has not
|
|
100
111
|
* been published. `cliAtLeast` treats an unreported version as capable, which is the convention
|
|
101
112
|
* every MIN_CLI gate here follows.
|
|
102
113
|
*/
|
|
103
|
-
export const LOCAL_APPLY_CONTRACT_MIN_CLI = "0.4.
|
|
114
|
+
export const LOCAL_APPLY_CONTRACT_MIN_CLI = "0.4.97";
|
|
104
115
|
export const LOCAL_APPLY_DIAGNOSTIC_CAUSES = ["bridge_unused", "engine_exited_nonzero", "timed_out", "no_engine_output", "submit_unconfirmed"];
|
|
105
116
|
export const LOCAL_APPLY_CONFIRMATION_MARKERS = ["page_text", "url_receipt", "already_applied_notice"];
|
|
106
117
|
export const LOCAL_APPLY_SIGNALS = [
|
|
@@ -140,6 +151,7 @@ export const LOCAL_APPLY_EVENT_TYPES = [
|
|
|
140
151
|
export const LOCAL_APPLY_ENGINE_AUTHS = ["machine-login", "subscription", "api-key", "missing_login", "unknown"];
|
|
141
152
|
export const LOCAL_APPLY_SUPERVISOR_DIRECTIVES = ["continue", "request_review", "stop"];
|
|
142
153
|
export const LOCAL_APPLY_SUPERVISOR_PHASES = ["initial", "before_submit", "post_navigation", "uncertain"];
|
|
154
|
+
export const LOCAL_APPLY_HANDOFF_TERMINAL_REASONS = ["unavailable", "profile_unavailable", "profile_mismatch", "page_lost", "expired", "run_ended", "runner_restarted"];
|
|
143
155
|
export const LOCAL_APPLY_CAPS = { questions: 20, questionChars: 300, answers: 20, answerChars: 2000, summary: 2000 };
|
|
144
156
|
const str = (v, max) => (typeof v === "string" && v.trim() ? v.trim().slice(0, max) : null);
|
|
145
157
|
const oneOf = (list, v) => (list.includes(v) ? v : null);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local-apply takeover surface is separate from generic task takeover: every request carries
|
|
3
|
+
* the exact run/application/profile binding that the runtime verifies before it reaches a page.
|
|
4
|
+
*/
|
|
5
|
+
export async function routeLocalApplyHandoff(runtime, method, path, read) {
|
|
6
|
+
if (method !== "POST")
|
|
7
|
+
return undefined;
|
|
8
|
+
if (path === "/local-apply/handoff")
|
|
9
|
+
return runtime.handoff(await read());
|
|
10
|
+
if (path === "/local-apply/handoff-status")
|
|
11
|
+
return runtime.handoffStatus(await read());
|
|
12
|
+
if (path === "/local-apply/handoff/frame")
|
|
13
|
+
return runtime.handoffFrame(await read());
|
|
14
|
+
if (path === "/local-apply/handoff/input") {
|
|
15
|
+
await runtime.handoffInput(await read());
|
|
16
|
+
return { ok: true };
|
|
17
|
+
}
|
|
18
|
+
if (path === "/local-apply/handoff/end" || path === "/local-apply/handoff/resume")
|
|
19
|
+
return runtime.endHandoff(await read());
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { RunnerInputError } from "../errors.js";
|
|
2
|
+
const SAFE_ID = /^[A-Za-z0-9_.:-]{1,300}$/;
|
|
3
|
+
const HANDOFF_TTL_MS = 10 * 60 * 1000;
|
|
4
|
+
/** Exact-page lifecycle only. It deliberately has no access to a bridge, browser credentials, or run output. */
|
|
5
|
+
export class LocalApplyHandoffRuntime {
|
|
6
|
+
host;
|
|
7
|
+
constructor(host) {
|
|
8
|
+
this.host = host;
|
|
9
|
+
}
|
|
10
|
+
async handoff(raw) {
|
|
11
|
+
const request = this.parse(raw);
|
|
12
|
+
const run = this.host.get(request.runId);
|
|
13
|
+
this.assertBinding(run, request);
|
|
14
|
+
if (run.state === "ended")
|
|
15
|
+
return this.closed(run, request.handoffId, "run_ended");
|
|
16
|
+
if (run.handoff)
|
|
17
|
+
throw new RunnerInputError("A handoff already exists for this application run", 409);
|
|
18
|
+
if (!this.host.takeover)
|
|
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");
|
|
23
|
+
try {
|
|
24
|
+
await this.host.takeover.open(request, page);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return this.closed(run, request.handoffId, "profile_unavailable");
|
|
28
|
+
}
|
|
29
|
+
run.handoff = { handoffId: request.handoffId, expiresAt: this.host.now() + (this.host.handoffTtlMs ?? HANDOFF_TTL_MS), state: "ready" };
|
|
30
|
+
return this.status(request);
|
|
31
|
+
}
|
|
32
|
+
async status(raw) {
|
|
33
|
+
const request = this.parse(raw);
|
|
34
|
+
const run = this.host.get(request.runId);
|
|
35
|
+
this.assertBinding(run, request);
|
|
36
|
+
if (run.state === "ended")
|
|
37
|
+
return this.closed(run, request.handoffId, "run_ended");
|
|
38
|
+
const handoff = run.handoff;
|
|
39
|
+
if (!handoff || handoff.handoffId !== request.handoffId)
|
|
40
|
+
return this.closed(run, request.handoffId, "unavailable");
|
|
41
|
+
if (handoff.state === "closed")
|
|
42
|
+
return this.response(run, handoff);
|
|
43
|
+
if (handoff.expiresAt <= this.host.now())
|
|
44
|
+
await this.close(run, "expired");
|
|
45
|
+
else if (!this.host.takeover || (await this.host.takeover.state(handoff.handoffId)) !== "ready")
|
|
46
|
+
await this.close(run, "page_lost");
|
|
47
|
+
return this.response(run, run.handoff);
|
|
48
|
+
}
|
|
49
|
+
async frame(raw) {
|
|
50
|
+
const { run, handoff } = await this.live(raw);
|
|
51
|
+
try {
|
|
52
|
+
return await this.host.takeover.frame(handoff.handoffId);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
await this.close(run, "page_lost");
|
|
56
|
+
throw new RunnerInputError("The handoff page is no longer available", 409);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async input(raw) {
|
|
60
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
61
|
+
const { run, handoff } = await this.live(o);
|
|
62
|
+
if (!o.input || typeof o.input !== "object")
|
|
63
|
+
throw new RunnerInputError("handoff input is required");
|
|
64
|
+
try {
|
|
65
|
+
await this.host.takeover.input(handoff.handoffId, o.input);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
await this.close(run, "page_lost");
|
|
69
|
+
throw new RunnerInputError("The handoff page is no longer available", 409);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async end(raw) {
|
|
73
|
+
const request = this.parse(raw);
|
|
74
|
+
const run = this.host.get(request.runId);
|
|
75
|
+
this.assertBinding(run, request);
|
|
76
|
+
if (!run.handoff || run.handoff.handoffId !== request.handoffId)
|
|
77
|
+
return this.closed(run, request.handoffId, "unavailable");
|
|
78
|
+
await this.close(run, "unavailable");
|
|
79
|
+
return this.response(run, run.handoff);
|
|
80
|
+
}
|
|
81
|
+
parse(raw) {
|
|
82
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
83
|
+
const handoffId = typeof o.handoffId === "string" ? o.handoffId : "";
|
|
84
|
+
const runId = typeof o.runId === "string" ? o.runId : "";
|
|
85
|
+
const applicationId = typeof o.applicationId === "string" ? o.applicationId : "";
|
|
86
|
+
const browserProfile = o.browserProfile === "default" || o.browserProfile === "isolated" ? o.browserProfile : null;
|
|
87
|
+
if (!SAFE_ID.test(handoffId) || !SAFE_ID.test(runId) || !SAFE_ID.test(applicationId) || !browserProfile)
|
|
88
|
+
throw new RunnerInputError("handoff needs opaque handoffId, runId, applicationId, and browserProfile");
|
|
89
|
+
return { handoffId, runId, applicationId, browserProfile };
|
|
90
|
+
}
|
|
91
|
+
assertBinding(run, request) {
|
|
92
|
+
if (run.envelope.applicationId !== request.applicationId || run.envelope.browserProfile !== request.browserProfile)
|
|
93
|
+
throw new RunnerInputError("The handoff does not match this application run", 403);
|
|
94
|
+
}
|
|
95
|
+
response(run, handoff) {
|
|
96
|
+
return { handoffId: handoff.handoffId, runId: run.envelope.runId, applicationId: run.envelope.applicationId, browserProfile: run.envelope.browserProfile, state: handoff.state, expiresAt: new Date(handoff.expiresAt).toISOString(), ...(handoff.terminalReason ? { terminalReason: handoff.terminalReason } : {}) };
|
|
97
|
+
}
|
|
98
|
+
closed(run, handoffId, terminalReason) {
|
|
99
|
+
const handoff = { handoffId, expiresAt: this.host.now(), state: "closed", terminalReason };
|
|
100
|
+
if (run.handoff?.handoffId === handoffId)
|
|
101
|
+
run.handoff = handoff;
|
|
102
|
+
return this.response(run, handoff);
|
|
103
|
+
}
|
|
104
|
+
async close(run, reason) {
|
|
105
|
+
const handoff = run.handoff;
|
|
106
|
+
if (!handoff || handoff.state === "closed")
|
|
107
|
+
return;
|
|
108
|
+
handoff.state = "closed";
|
|
109
|
+
handoff.terminalReason = reason;
|
|
110
|
+
if (this.host.takeover)
|
|
111
|
+
await this.host.takeover.end(handoff.handoffId).catch(() => undefined);
|
|
112
|
+
}
|
|
113
|
+
async live(raw) {
|
|
114
|
+
const status = await this.status(raw);
|
|
115
|
+
if (status.state !== "ready")
|
|
116
|
+
throw new RunnerInputError(`The handoff is closed (${status.terminalReason ?? "unavailable"})`, 409);
|
|
117
|
+
const run = this.host.get(status.runId);
|
|
118
|
+
return { run, handoff: run.handoff };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -37,6 +37,7 @@ import { ApplyBridge } from "./bridge.js";
|
|
|
37
37
|
import { confirmationSignals } from "./confirmation.js";
|
|
38
38
|
import { LOCAL_APPLY_AUTH_MODES, LOCAL_APPLY_CAPS, LOCAL_APPLY_ENGINES, LOCAL_APPLY_MODES, LOCAL_APPLY_TASK_TYPE, parseLocalApplyResult, } from "./contract.js";
|
|
39
39
|
import { applyPrompt, buildApplyEngineSpec, observedApplyAuth } from "./engine.js";
|
|
40
|
+
import { LocalApplyHandoffRuntime } from "./runtime-handoff.js";
|
|
40
41
|
const MAX_EVENTS = 1000;
|
|
41
42
|
const OUTPUT_LINES = 400;
|
|
42
43
|
const SOURCE_BYTES = 256 * 1024;
|
|
@@ -169,6 +170,7 @@ function bridgeUnusedHelp(engine, d) {
|
|
|
169
170
|
export class LocalApplyRuntime {
|
|
170
171
|
deps;
|
|
171
172
|
runs = new Map();
|
|
173
|
+
handoffs;
|
|
172
174
|
/** Application fills that a restart would lose (#896) — their state is in this process. */
|
|
173
175
|
liveWork() {
|
|
174
176
|
return [...this.runs.values()].filter((r) => r.state !== "ended").map((r) => `an application fill (${r.envelope.applicationId.slice(0, 8)})`);
|
|
@@ -183,6 +185,7 @@ export class LocalApplyRuntime {
|
|
|
183
185
|
this.now = deps.now ?? Date.now;
|
|
184
186
|
this.spawn = deps.spawn ?? nodeSpawn;
|
|
185
187
|
this.retentionMs = deps.retentionMs ?? 24 * 60 * 60 * 1000;
|
|
188
|
+
this.handoffs = new LocalApplyHandoffRuntime({ get: (runId) => this.get(runId), now: this.now, takeover: deps.takeover, handoffTtlMs: deps.handoffTtlMs });
|
|
186
189
|
this.hydrateTerminalJournals();
|
|
187
190
|
}
|
|
188
191
|
/**
|
|
@@ -650,6 +653,12 @@ export class LocalApplyRuntime {
|
|
|
650
653
|
...(run.result ? { result: run.result } : {}),
|
|
651
654
|
};
|
|
652
655
|
}
|
|
656
|
+
/** A takeover only views the exact live page; it never authorizes the paused bridge. */
|
|
657
|
+
async handoff(raw) { return this.handoffs.handoff(raw); }
|
|
658
|
+
async handoffStatus(raw) { return this.handoffs.status(raw); }
|
|
659
|
+
async handoffFrame(raw) { return this.handoffs.frame(raw); }
|
|
660
|
+
async handoffInput(raw) { return this.handoffs.input(raw); }
|
|
661
|
+
async endHandoff(raw) { return this.handoffs.end(raw); }
|
|
653
662
|
/** Does this token belong to this run? The bridge forwarder's only credential. */
|
|
654
663
|
authorizeBridge(runId, token) {
|
|
655
664
|
const run = this.runs.get(runId);
|
|
@@ -689,6 +698,10 @@ export class LocalApplyRuntime {
|
|
|
689
698
|
end(run, r) {
|
|
690
699
|
if (run.state === "ended")
|
|
691
700
|
return;
|
|
701
|
+
// A terminal application outcome never means that a handoff succeeded. Tear down only the
|
|
702
|
+
// live view and retain the explicit closed state for a caller that polls before restart.
|
|
703
|
+
if (run.handoff?.state === "ready")
|
|
704
|
+
void this.handoffs.close(run, "run_ended");
|
|
692
705
|
if (run.state === "paused")
|
|
693
706
|
this.release(run, "stopped");
|
|
694
707
|
if (run.timer)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RunnerInputError } from "../errors.js";
|
|
2
|
+
const prefix = "local-apply:";
|
|
3
|
+
export function isLocalApplyTakeover(id) { return id.startsWith(prefix); }
|
|
4
|
+
/** Attach the bounded local-apply view to the runner's existing live-page takeover machinery. */
|
|
5
|
+
export function createLocalApplyTakeoverAdapter(deps) {
|
|
6
|
+
const key = (handoffId) => `${prefix}${handoffId}`;
|
|
7
|
+
return {
|
|
8
|
+
async open(request, page) {
|
|
9
|
+
const id = key(request.handoffId);
|
|
10
|
+
if (deps.get(id))
|
|
11
|
+
throw new RunnerInputError("A local application handoff already exists", 409);
|
|
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.
|
|
14
|
+
if (!page || page.isClosed())
|
|
15
|
+
throw new RunnerInputError("The local application page is no longer available", 409);
|
|
16
|
+
deps.set(id, page);
|
|
17
|
+
},
|
|
18
|
+
async state(handoffId) {
|
|
19
|
+
const session = deps.get(key(handoffId));
|
|
20
|
+
return session && !session.page.isClosed() ? "ready" : "page_lost";
|
|
21
|
+
},
|
|
22
|
+
frame: (handoffId) => deps.frame(key(handoffId)),
|
|
23
|
+
input: (handoffId, input) => deps.input(key(handoffId), input),
|
|
24
|
+
end: (handoffId) => deps.end(key(handoffId)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -18,6 +18,8 @@ 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";
|
|
22
|
+
import { createLocalApplyTakeoverAdapter, isLocalApplyTakeover } from "./local-apply/takeover-adapter.js";
|
|
21
23
|
import { WORKFLOW_DRIVEN_TASKS } from "./task-types.js";
|
|
22
24
|
/** True for a plain object. */
|
|
23
25
|
function isRecord(value) {
|
|
@@ -103,9 +105,15 @@ export class LocalRunner {
|
|
|
103
105
|
await mcp.start({ userDataDir: join(runDir, "profile"), headless: config.headless });
|
|
104
106
|
return { tools: mcp, stop: () => mcp.stop() };
|
|
105
107
|
};
|
|
108
|
+
const applyBrowserFor = createLocalApplyBrowserFactory({ headless: config.headless, defaultBrowser: browserFor, sharedPage: () => this.activePage });
|
|
106
109
|
this.localBrowser = new LocalBrowserRuntime({ dataDir: config.dataDir, selfUrl: () => this.selfUrl, browserFor });
|
|
107
110
|
this.localArtifact = new LocalArtifactRuntime({ dataDir: config.dataDir });
|
|
108
|
-
this.localApply = new LocalApplyRuntime({
|
|
111
|
+
this.localApply = new LocalApplyRuntime({
|
|
112
|
+
dataDir: config.dataDir,
|
|
113
|
+
selfUrl: () => this.selfUrl,
|
|
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) }),
|
|
116
|
+
});
|
|
109
117
|
// Tasks paused/running on a previous process are orphaned now — their
|
|
110
118
|
// pages and takeover sessions are gone. Fail them so the board is clean.
|
|
111
119
|
const expired = this.store.expireInFlightTasks();
|
|
@@ -365,7 +373,7 @@ export class LocalRunner {
|
|
|
365
373
|
}
|
|
366
374
|
/** Active human-takeover task ids (for status/discovery). */
|
|
367
375
|
listTakeovers() {
|
|
368
|
-
return [...this.takeovers.keys()];
|
|
376
|
+
return [...this.takeovers.keys()].filter((id) => !isLocalApplyTakeover(id));
|
|
369
377
|
}
|
|
370
378
|
requireTakeover(taskId) {
|
|
371
379
|
const session = this.takeovers.get(taskId);
|
|
@@ -4,6 +4,7 @@ import { LocalRunner, RunnerInputError } from "./runner.js";
|
|
|
4
4
|
import { probeGitSshIdentity } from "./coding/repo.js";
|
|
5
5
|
import { checkEngine } from "./coding/engine-check.js";
|
|
6
6
|
import { listGithubOrgs, listGithubRepos, searchGithubRepos, getGithubRepoDetail, getGithubCredentialScope } from "./coding/github-browse.js";
|
|
7
|
+
import { routeLocalApplyHandoff } from "./local-apply/handoff-routes.js";
|
|
7
8
|
export function createRunnerServer(runner) {
|
|
8
9
|
return createServer(async (req, res) => {
|
|
9
10
|
try {
|
|
@@ -226,7 +227,6 @@ async function route(runner, req, res) {
|
|
|
226
227
|
return json(res, 200, runner.localArtifact.cancel(await readJson(req)));
|
|
227
228
|
}
|
|
228
229
|
// Local application execution (#957) — run, status, resume, cancel; PULL like research.
|
|
229
|
-
// The email-lead preflight is read-only and precedes Tailor material generation (#953).
|
|
230
230
|
if (req.method === "POST" && path === "/local-apply/preflight") {
|
|
231
231
|
return json(res, 200, await runner.localApply.preflight(await readJson(req)));
|
|
232
232
|
}
|
|
@@ -236,6 +236,9 @@ async function route(runner, req, res) {
|
|
|
236
236
|
if (req.method === "POST" && path === "/local-apply/status") {
|
|
237
237
|
return json(res, 200, runner.localApply.status(await readJson(req)));
|
|
238
238
|
}
|
|
239
|
+
const handoff = await routeLocalApplyHandoff(runner.localApply, req.method, path, () => readJson(req));
|
|
240
|
+
if (handoff)
|
|
241
|
+
return json(res, 200, handoff);
|
|
239
242
|
if (req.method === "POST" && path === "/local-apply/resume") {
|
|
240
243
|
return json(res, 200, runner.localApply.resume(await readJson(req)));
|
|
241
244
|
}
|
package/dist/index.js
CHANGED
|
@@ -1875,8 +1875,7 @@ var RelayMutationAdmission = class {
|
|
|
1875
1875
|
}
|
|
1876
1876
|
};
|
|
1877
1877
|
|
|
1878
|
-
// src/commands/runner/
|
|
1879
|
-
var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
|
|
1878
|
+
// src/commands/runner/runner-work-observation.ts
|
|
1880
1879
|
function observedRunnerWork(observation) {
|
|
1881
1880
|
const sessions = observation.sessions;
|
|
1882
1881
|
const work = observation.work;
|
|
@@ -1900,6 +1899,9 @@ function observedRunnerWork(observation) {
|
|
|
1900
1899
|
...Array.from({ length: localRuns }, (_v, i) => localDetail[i] || `local-run-${i + 1}`)
|
|
1901
1900
|
];
|
|
1902
1901
|
}
|
|
1902
|
+
|
|
1903
|
+
// src/commands/runner/relay.ts
|
|
1904
|
+
var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
|
|
1903
1905
|
var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH]);
|
|
1904
1906
|
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
|
|
1905
1907
|
const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
|