@proagentstore/cli 0.4.97 → 0.4.98
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 +12 -0
- package/dist/browser-runner/local-apply/live-run-browser.js +14 -0
- package/dist/browser-runner/local-apply/reconciliation-routes.js +24 -0
- package/dist/browser-runner/local-apply/reconciliation-runtime.js +210 -0
- package/dist/browser-runner/runner.js +10 -13
- package/dist/browser-runner/server.js +4 -0
- package/package.json +1 -1
|
@@ -46,6 +46,18 @@ export const LOCAL_APPLY_HANDOFF_FRAME_PATH = "/local-apply/handoff/frame";
|
|
|
46
46
|
export const LOCAL_APPLY_HANDOFF_INPUT_PATH = "/local-apply/handoff/input";
|
|
47
47
|
export const LOCAL_APPLY_HANDOFF_RESUME_PATH = "/local-apply/handoff/resume";
|
|
48
48
|
export const LOCAL_APPLY_HANDOFF_END_PATH = "/local-apply/handoff/end";
|
|
49
|
+
/**
|
|
50
|
+
* A separate, browser-only receipt/history check for an already-ended uncertain attempt. This is
|
|
51
|
+
* deliberately not an application run: it receives no engine, sources, artifacts or submit gate.
|
|
52
|
+
*/
|
|
53
|
+
export const LOCAL_APPLY_RECONCILIATION_RUN_PATH = "/local-apply/reconciliation/run";
|
|
54
|
+
export const LOCAL_APPLY_RECONCILIATION_STATUS_PATH = "/local-apply/reconciliation/status";
|
|
55
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_PATH = "/local-apply/reconciliation/handoff";
|
|
56
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_STATUS_PATH = "/local-apply/reconciliation/handoff-status";
|
|
57
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_FRAME_PATH = "/local-apply/reconciliation/handoff/frame";
|
|
58
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_INPUT_PATH = "/local-apply/reconciliation/handoff/input";
|
|
59
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_RESUME_PATH = "/local-apply/reconciliation/handoff/resume";
|
|
60
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_END_PATH = "/local-apply/reconciliation/handoff/end";
|
|
49
61
|
export const LOCAL_APPLY_ENGINES = ["claude", "codex"];
|
|
50
62
|
export const LOCAL_APPLY_AUTH_MODES = ["machine", "subscription"];
|
|
51
63
|
export const LOCAL_APPLY_MODES = ["fill_and_review", "auto_submit"];
|
|
@@ -10,6 +10,20 @@ export function createLocalApplyBrowserFactory(options) {
|
|
|
10
10
|
return { ...browser, handoffPage: options.sharedPage };
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* 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.
|
|
16
|
+
*/
|
|
17
|
+
export function createRunnerBrowserFactories(options) {
|
|
18
|
+
const browserFor = async (profile, runDir) => {
|
|
19
|
+
if (profile === "default")
|
|
20
|
+
return { tools: await options.defaultTools(), stop: async () => undefined };
|
|
21
|
+
const mcp = new McpRuntime();
|
|
22
|
+
await mcp.start({ userDataDir: join(runDir, "profile"), headless: options.headless });
|
|
23
|
+
return { tools: mcp, stop: () => mcp.stop() };
|
|
24
|
+
};
|
|
25
|
+
return { browserFor, applyBrowserFor: createLocalApplyBrowserFactory({ headless: options.headless, defaultBrowser: browserFor, sharedPage: options.sharedPage }) };
|
|
26
|
+
}
|
|
13
27
|
/**
|
|
14
28
|
* Start the disposable browser context for one application run and retain only its current live
|
|
15
29
|
* page for the bounded takeover adapter. This deliberately does not seed, copy, or persist an
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Separate from apply and generic tasks: this surface has no bridge/fill/submit operation. */
|
|
2
|
+
export async function routeLocalApplyReconciliation(runtime, method, path, read) {
|
|
3
|
+
if (method !== "POST")
|
|
4
|
+
return undefined;
|
|
5
|
+
if (path === "/local-apply/reconciliation/run")
|
|
6
|
+
return runtime.start(await read());
|
|
7
|
+
if (path === "/local-apply/reconciliation/status")
|
|
8
|
+
return runtime.status(await read());
|
|
9
|
+
if (path === "/local-apply/reconciliation/handoff")
|
|
10
|
+
return runtime.handoff(await read());
|
|
11
|
+
if (path === "/local-apply/reconciliation/handoff-status")
|
|
12
|
+
return runtime.status(await read());
|
|
13
|
+
if (path === "/local-apply/reconciliation/handoff/frame")
|
|
14
|
+
return runtime.handoffFrame(await read());
|
|
15
|
+
if (path === "/local-apply/reconciliation/handoff/input") {
|
|
16
|
+
await runtime.handoffInput(await read());
|
|
17
|
+
return { ok: true };
|
|
18
|
+
}
|
|
19
|
+
if (path === "/local-apply/reconciliation/handoff/resume")
|
|
20
|
+
return runtime.resume(await read());
|
|
21
|
+
if (path === "/local-apply/reconciliation/handoff/end")
|
|
22
|
+
return runtime.end(await read());
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only reconciliation for an ended `submit_unconfirmed` attempt.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally independent of ApplyBridge: it launches no CLI, receives no source or
|
|
5
|
+
* artifact paths and has no action surface capable of filling, uploading or submitting. The only
|
|
6
|
+
* page inspection remains in this process and returns a closed evidence vocabulary to PAGS.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { RunnerInputError } from "../errors.js";
|
|
10
|
+
import { domainWithin } from "../local-browser/bridge.js";
|
|
11
|
+
const SAFE_ID = /^[A-Za-z0-9_.:-]{1,300}$/;
|
|
12
|
+
const TTL_MS = 10 * 60_000;
|
|
13
|
+
/** Dedicated runtime; there is deliberately no bridge, engine, or generic browser-task fallback. */
|
|
14
|
+
export class LocalApplyReconciliationRuntime {
|
|
15
|
+
deps;
|
|
16
|
+
sessions = new Map();
|
|
17
|
+
now;
|
|
18
|
+
constructor(deps) {
|
|
19
|
+
this.deps = deps;
|
|
20
|
+
this.now = deps.now ?? (() => Date.now());
|
|
21
|
+
}
|
|
22
|
+
async start(raw) {
|
|
23
|
+
const envelope = parseEnvelope(raw);
|
|
24
|
+
const existing = this.sessions.get(envelope.reconciliationId);
|
|
25
|
+
if (existing) {
|
|
26
|
+
if (existing.envelope.runId !== envelope.runId || existing.envelope.applicationId !== envelope.applicationId || existing.envelope.instanceId !== envelope.instanceId || existing.envelope.browserProfile !== envelope.browserProfile)
|
|
27
|
+
throw new RunnerInputError("The reconciliation id is already bound to another attempt", 409);
|
|
28
|
+
return this.view(existing);
|
|
29
|
+
}
|
|
30
|
+
const browser = await this.deps.browserFor(envelope.browserProfile, join(this.deps.dataDir, "reconciliation", envelope.reconciliationId));
|
|
31
|
+
const session = { envelope, browser, state: "running", expiresAt: this.now() + TTL_MS };
|
|
32
|
+
this.sessions.set(envelope.reconciliationId, session);
|
|
33
|
+
try {
|
|
34
|
+
const page = this.page(session);
|
|
35
|
+
if (!page)
|
|
36
|
+
return await this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
37
|
+
await page.goto(envelope.applicationUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
38
|
+
return await this.inspect(session);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async status(raw) {
|
|
45
|
+
const session = this.require(raw);
|
|
46
|
+
if (session.state === "ended")
|
|
47
|
+
return this.view(session);
|
|
48
|
+
if (session.expiresAt <= this.now())
|
|
49
|
+
return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
|
|
50
|
+
if (!this.page(session))
|
|
51
|
+
return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
52
|
+
if (session.handoff?.state === "ready" && (session.handoff.expiresAt <= this.now() || !this.deps.takeover || await this.deps.takeover.state(session.handoff.id) !== "ready"))
|
|
53
|
+
await this.closeHandoff(session, session.handoff.expiresAt <= this.now() ? "expired" : "page_lost");
|
|
54
|
+
return this.view(session);
|
|
55
|
+
}
|
|
56
|
+
async handoff(raw) {
|
|
57
|
+
const request = this.handoffRequest(raw);
|
|
58
|
+
const session = this.require(request);
|
|
59
|
+
this.assertHandoffBinding(session, request);
|
|
60
|
+
if (session.state !== "paused" || !session.pauseReason)
|
|
61
|
+
throw new RunnerInputError("Reconciliation handoff is available only for a live login or CAPTCHA pause", 409);
|
|
62
|
+
if (session.handoff)
|
|
63
|
+
throw new RunnerInputError("A reconciliation handoff already exists", 409);
|
|
64
|
+
const page = this.page(session);
|
|
65
|
+
if (!page || !this.deps.takeover)
|
|
66
|
+
return this.closedHandoff(session, request.handoffId, "profile_unavailable");
|
|
67
|
+
try {
|
|
68
|
+
await this.deps.takeover.open(request, page);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return this.closedHandoff(session, request.handoffId, "page_lost");
|
|
72
|
+
}
|
|
73
|
+
session.handoff = { id: request.handoffId, expiresAt: this.now() + TTL_MS, state: "ready" };
|
|
74
|
+
return this.view(session);
|
|
75
|
+
}
|
|
76
|
+
async handoffFrame(raw) {
|
|
77
|
+
const session = await this.liveHandoff(raw);
|
|
78
|
+
try {
|
|
79
|
+
return await this.deps.takeover.frame(session.handoff.id);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
await this.closeHandoff(session, "page_lost");
|
|
83
|
+
throw new RunnerInputError("The reconciliation page is no longer available", 409);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async handoffInput(raw) {
|
|
87
|
+
const request = raw && typeof raw === "object" ? raw : {};
|
|
88
|
+
const session = await this.liveHandoff(request);
|
|
89
|
+
if (!request.input || typeof request.input !== "object")
|
|
90
|
+
throw new RunnerInputError("handoff input is required");
|
|
91
|
+
// A person may authenticate/solve a CAPTCHA, but once that wall is gone this session accepts
|
|
92
|
+
// no further controls. It can never become an application fill/submit surface.
|
|
93
|
+
const blocker = await this.blocker(this.page(session));
|
|
94
|
+
if (!blocker)
|
|
95
|
+
throw new RunnerInputError("The read-only reconciliation handoff no longer accepts browser input", 409);
|
|
96
|
+
try {
|
|
97
|
+
await this.deps.takeover.input(session.handoff.id, request.input);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
await this.closeHandoff(session, "page_lost");
|
|
101
|
+
throw new RunnerInputError("The reconciliation page is no longer available", 409);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async resume(raw) {
|
|
105
|
+
const session = await this.liveHandoff(raw);
|
|
106
|
+
return this.inspect(session);
|
|
107
|
+
}
|
|
108
|
+
async end(raw) {
|
|
109
|
+
const session = this.require(raw);
|
|
110
|
+
if (session.handoff?.state === "ready")
|
|
111
|
+
await this.closeHandoff(session, "unavailable");
|
|
112
|
+
session.state = "running";
|
|
113
|
+
session.pauseReason = undefined;
|
|
114
|
+
return this.view(session);
|
|
115
|
+
}
|
|
116
|
+
liveWork() { return [...this.sessions.values()].filter((s) => s.state !== "ended").map((s) => `reconciliation:${s.envelope.reconciliationId}`); }
|
|
117
|
+
closeAll() { for (const session of this.sessions.values())
|
|
118
|
+
void this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" }); }
|
|
119
|
+
async inspect(session) {
|
|
120
|
+
const page = this.page(session);
|
|
121
|
+
if (!page)
|
|
122
|
+
return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
123
|
+
const state = await this.pageState(page).catch(() => null);
|
|
124
|
+
if (!state || !this.permitted(session, state.url))
|
|
125
|
+
return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
|
|
126
|
+
if (state.login || state.captcha) {
|
|
127
|
+
session.state = "paused";
|
|
128
|
+
session.pauseReason = state.captcha ? "captcha" : "login_required";
|
|
129
|
+
return this.view(session);
|
|
130
|
+
}
|
|
131
|
+
// A handoff which has just cleared authentication is the one case where we retain the
|
|
132
|
+
// bounded browser. It lets the owner inspect their authenticated account/job view without
|
|
133
|
+
// turning generic page prose into an outcome. Explicit end or the ten-minute deadline
|
|
134
|
+
// resolves it as ambiguous unless a future provider-specific verifier supplies exact proof.
|
|
135
|
+
if (session.handoff?.state === "ready") {
|
|
136
|
+
session.state = "running";
|
|
137
|
+
session.pauseReason = undefined;
|
|
138
|
+
return this.view(session);
|
|
139
|
+
}
|
|
140
|
+
// SEEK has no validated account/job receipt or history contract in this runtime. Generic
|
|
141
|
+
// prose, receipt-like URLs, unauthenticated pages and absent receipts prove nothing.
|
|
142
|
+
return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
|
|
143
|
+
}
|
|
144
|
+
async finish(session, result) {
|
|
145
|
+
if (session.state === "ended")
|
|
146
|
+
return this.view(session);
|
|
147
|
+
if (session.handoff?.state === "ready")
|
|
148
|
+
await this.closeHandoff(session, "unavailable");
|
|
149
|
+
session.state = "ended";
|
|
150
|
+
session.pauseReason = undefined;
|
|
151
|
+
session.result = result;
|
|
152
|
+
await session.browser.stop().catch(() => undefined);
|
|
153
|
+
return this.view(session);
|
|
154
|
+
}
|
|
155
|
+
async closeHandoff(session, reason) {
|
|
156
|
+
if (!session.handoff || session.handoff.state === "closed")
|
|
157
|
+
return;
|
|
158
|
+
session.handoff.state = "closed";
|
|
159
|
+
session.handoff.reason = reason;
|
|
160
|
+
await this.deps.takeover?.end(session.handoff.id).catch(() => undefined);
|
|
161
|
+
}
|
|
162
|
+
closedHandoff(session, id, reason) { session.handoff = { id, expiresAt: this.now(), state: "closed", reason }; return this.view(session); }
|
|
163
|
+
page(session) { const page = session.browser.handoffPage?.(); return page && !page.isClosed() ? page : null; }
|
|
164
|
+
permitted(session, raw) { try {
|
|
165
|
+
const host = new URL(raw).hostname.toLowerCase();
|
|
166
|
+
return session.envelope.allowDomains.some((d) => domainWithin(host, d));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return false;
|
|
170
|
+
} }
|
|
171
|
+
async blocker(page) { const state = page ? await this.pageState(page).catch(() => null) : null; return !!state && (state.login || state.captcha); }
|
|
172
|
+
async pageState(page) {
|
|
173
|
+
return page.evaluate(() => {
|
|
174
|
+
const text = (document.body?.innerText ?? "").toLowerCase();
|
|
175
|
+
const captcha = !!document.querySelector('iframe[src*="hcaptcha.com"], .h-captcha, iframe[src*="challenges.cloudflare.com"], .cf-turnstile, iframe[src*="arkoselabs"], .geetest_holder') || /confirm (that )?you('?re| are) not a robot|i'?m not a robot|verify (that )?you('?re| are) (a )?human|checking (if the site connection is secure|your browser)/.test(text);
|
|
176
|
+
// Kept in the page: site prose never crosses the runner boundary. This is deliberately
|
|
177
|
+
// narrower than a guess; anything unrecognised becomes `ambiguous`.
|
|
178
|
+
const password = !!document.querySelector("input[type=password]");
|
|
179
|
+
const otp = !!document.querySelector("input[autocomplete='one-time-code'], input[name*='otp' i], input[name*='verification' i], input[id*='otp' i], input[id*='verification' i]");
|
|
180
|
+
const emailSignIn = !!document.querySelector("form[action*='login' i] input[type=email], form[action*='sign-in' i] input[type=email], [data-auth-screen] input[type=email]");
|
|
181
|
+
return { url: location.href, login: password || otp || emailSignIn, captcha };
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
require(raw) { const id = typeof raw?.reconciliationId === "string" ? raw.reconciliationId : ""; if (!SAFE_ID.test(id))
|
|
185
|
+
throw new RunnerInputError("reconciliationId is required"); const session = this.sessions.get(id); if (!session)
|
|
186
|
+
throw new RunnerInputError("Reconciliation session not found", 404); return session; }
|
|
187
|
+
handoffRequest(raw) { const o = raw && typeof raw === "object" ? raw : {}; const handoffId = typeof o.handoffId === "string" ? o.handoffId : ""; const runId = typeof o.runId === "string" ? o.runId : ""; const applicationId = typeof o.applicationId === "string" ? o.applicationId : ""; const reconciliationId = typeof o.reconciliationId === "string" ? o.reconciliationId : ""; const browserProfile = o.browserProfile === "isolated" || o.browserProfile === "default" ? o.browserProfile : null; if (![handoffId, runId, applicationId, reconciliationId].every((v) => SAFE_ID.test(v)) || !browserProfile)
|
|
188
|
+
throw new RunnerInputError("reconciliation handoff needs opaque ids and browserProfile"); return { handoffId, runId, applicationId, reconciliationId, browserProfile }; }
|
|
189
|
+
assertHandoffBinding(session, request) { const e = session.envelope; if (e.reconciliationId !== request.reconciliationId || e.runId !== request.runId || e.applicationId !== request.applicationId || e.browserProfile !== request.browserProfile)
|
|
190
|
+
throw new RunnerInputError("The reconciliation handoff does not match this exact attempt", 403); }
|
|
191
|
+
async liveHandoff(raw) { const request = this.handoffRequest(raw); const session = this.require(request); this.assertHandoffBinding(session, request); if ((session.state !== "paused" && session.state !== "running") || session.handoff?.id !== request.handoffId || session.handoff.state !== "ready")
|
|
192
|
+
throw new RunnerInputError("The reconciliation handoff is closed", 409); if (session.handoff.expiresAt <= this.now()) {
|
|
193
|
+
await this.closeHandoff(session, "expired");
|
|
194
|
+
throw new RunnerInputError("The reconciliation handoff is closed", 409);
|
|
195
|
+
} return session; }
|
|
196
|
+
view(session) { const e = session.envelope; return { reconciliationId: e.reconciliationId, runId: e.runId, applicationId: e.applicationId, browserProfile: e.browserProfile, context: "separate_read_only_context", state: session.state, ...(session.pauseReason ? { pauseReason: session.pauseReason } : {}), ...(session.result ? { result: session.result } : {}) }; }
|
|
197
|
+
}
|
|
198
|
+
function parseEnvelope(raw) {
|
|
199
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
200
|
+
const ids = ["reconciliationId", "runId", "instanceId", "applicationId"];
|
|
201
|
+
for (const key of ids)
|
|
202
|
+
if (typeof o[key] !== "string" || !SAFE_ID.test(o[key]))
|
|
203
|
+
throw new RunnerInputError(`Invalid reconciliation: ${key} is required`);
|
|
204
|
+
if (o.type !== "local_browser.apply.reconciliation" || (o.browserProfile !== "isolated" && o.browserProfile !== "default") || typeof o.applicationUrl !== "string" || !/^https?:\/\/[^\s/]+/i.test(o.applicationUrl))
|
|
205
|
+
throw new RunnerInputError("Invalid read-only reconciliation envelope");
|
|
206
|
+
const allowDomains = Array.isArray(o.allowDomains) ? o.allowDomains.filter((d) => typeof d === "string" && /^[a-z0-9.-]+$/i.test(d)).map((d) => d.toLowerCase()) : [];
|
|
207
|
+
if (!allowDomains.length || typeof o.jobIdentity !== "string" || !o.materialFingerprint || typeof o.materialFingerprint !== "object")
|
|
208
|
+
throw new RunnerInputError("Invalid reconciliation binding");
|
|
209
|
+
return { type: o.type, reconciliationId: o.reconciliationId, runId: o.runId, instanceId: o.instanceId, applicationId: o.applicationId, browserProfile: o.browserProfile, applicationUrl: o.applicationUrl, allowDomains, jobIdentity: o.jobIdentity, materialFingerprint: o.materialFingerprint };
|
|
210
|
+
}
|
|
@@ -17,8 +17,9 @@ import { LOCAL_BROWSER_TASK_TYPE } from "./local-browser/contract.js";
|
|
|
17
17
|
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
|
+
import { LocalApplyReconciliationRuntime } from "./local-apply/reconciliation-runtime.js";
|
|
20
21
|
import { LOCAL_APPLY_TASK_TYPE } from "./local-apply/contract.js";
|
|
21
|
-
import {
|
|
22
|
+
import { createRunnerBrowserFactories } from "./local-apply/live-run-browser.js";
|
|
22
23
|
import { createLocalApplyTakeoverAdapter, isLocalApplyTakeover } from "./local-apply/takeover-adapter.js";
|
|
23
24
|
import { WORKFLOW_DRIVEN_TASKS } from "./task-types.js";
|
|
24
25
|
/** True for a plain object. */
|
|
@@ -87,6 +88,8 @@ export class LocalRunner {
|
|
|
87
88
|
localArtifact;
|
|
88
89
|
/** Local application execution (#957): a signed-in CLI filling one application through the apply bridge. */
|
|
89
90
|
localApply;
|
|
91
|
+
/** Browser-only, no-bridge reconciliation for an ended uncertain apply attempt. */
|
|
92
|
+
localApplyReconciliation;
|
|
90
93
|
/** This runner's own local URL, once the server listens — the bridge forwarder calls back to it. */
|
|
91
94
|
selfUrl = null;
|
|
92
95
|
/** Live human-takeover sessions, keyed by task id (the page is kept alive). */
|
|
@@ -96,24 +99,17 @@ export class LocalRunner {
|
|
|
96
99
|
mkdirSync(config.dataDir, { recursive: true });
|
|
97
100
|
this.store = new RunnerStore(config.dataDir);
|
|
98
101
|
this.coding = new CodingRuntime(join(config.dataDir, "repos"));
|
|
99
|
-
|
|
100
|
-
// `isolated` is a fresh profile inside the run's folder, removed with it.
|
|
101
|
-
const browserFor = async (profile, runDir) => {
|
|
102
|
-
if (profile === "default")
|
|
103
|
-
return { tools: await this.getMcp(), stop: async () => undefined };
|
|
104
|
-
const mcp = new McpRuntime();
|
|
105
|
-
await mcp.start({ userDataDir: join(runDir, "profile"), headless: config.headless });
|
|
106
|
-
return { tools: mcp, stop: () => mcp.stop() };
|
|
107
|
-
};
|
|
108
|
-
const applyBrowserFor = createLocalApplyBrowserFactory({ headless: config.headless, defaultBrowser: browserFor, sharedPage: () => this.activePage });
|
|
102
|
+
const { browserFor, applyBrowserFor } = createRunnerBrowserFactories({ headless: config.headless, defaultTools: () => this.getMcp(), sharedPage: () => this.activePage });
|
|
109
103
|
this.localBrowser = new LocalBrowserRuntime({ dataDir: config.dataDir, selfUrl: () => this.selfUrl, browserFor });
|
|
110
104
|
this.localArtifact = new LocalArtifactRuntime({ dataDir: config.dataDir });
|
|
105
|
+
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) });
|
|
111
106
|
this.localApply = new LocalApplyRuntime({
|
|
112
107
|
dataDir: config.dataDir,
|
|
113
108
|
selfUrl: () => this.selfUrl,
|
|
114
109
|
browserFor: applyBrowserFor,
|
|
115
|
-
takeover:
|
|
110
|
+
takeover: localApplyTakeover,
|
|
116
111
|
});
|
|
112
|
+
this.localApplyReconciliation = new LocalApplyReconciliationRuntime({ dataDir: config.dataDir, browserFor: applyBrowserFor, takeover: localApplyTakeover });
|
|
117
113
|
// Tasks paused/running on a previous process are orphaned now — their
|
|
118
114
|
// pages and takeover sessions are gone. Fail them so the board is clean.
|
|
119
115
|
const expired = this.store.expireInFlightTasks();
|
|
@@ -131,7 +127,7 @@ export class LocalRunner {
|
|
|
131
127
|
*/
|
|
132
128
|
liveWork() {
|
|
133
129
|
const coding = this.coding.liveWork();
|
|
134
|
-
const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork()];
|
|
130
|
+
const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork(), ...this.localApplyReconciliation.liveWork()];
|
|
135
131
|
return { codingTurns: coding.length, localRuns: local.length, detail: [...coding, ...local].slice(0, 10) };
|
|
136
132
|
}
|
|
137
133
|
capabilities() {
|
|
@@ -292,6 +288,7 @@ export class LocalRunner {
|
|
|
292
288
|
this.localBrowser.closeAll();
|
|
293
289
|
this.localArtifact.closeAll();
|
|
294
290
|
this.localApply.closeAll();
|
|
291
|
+
this.localApplyReconciliation.closeAll();
|
|
295
292
|
try {
|
|
296
293
|
this.coding.closeAll();
|
|
297
294
|
}
|
|
@@ -5,6 +5,7 @@ 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
7
|
import { routeLocalApplyHandoff } from "./local-apply/handoff-routes.js";
|
|
8
|
+
import { routeLocalApplyReconciliation } from "./local-apply/reconciliation-routes.js";
|
|
8
9
|
export function createRunnerServer(runner) {
|
|
9
10
|
return createServer(async (req, res) => {
|
|
10
11
|
try {
|
|
@@ -239,6 +240,9 @@ async function route(runner, req, res) {
|
|
|
239
240
|
const handoff = await routeLocalApplyHandoff(runner.localApply, req.method, path, () => readJson(req));
|
|
240
241
|
if (handoff)
|
|
241
242
|
return json(res, 200, handoff);
|
|
243
|
+
const reconciliation = await routeLocalApplyReconciliation(runner.localApplyReconciliation, req.method, path, () => readJson(req));
|
|
244
|
+
if (reconciliation !== undefined)
|
|
245
|
+
return json(res, 200, reconciliation);
|
|
242
246
|
if (req.method === "POST" && path === "/local-apply/resume") {
|
|
243
247
|
return json(res, 200, runner.localApply.resume(await readJson(req)));
|
|
244
248
|
}
|