@proagentstore/cli 0.4.133 → 0.4.134

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.
@@ -60,6 +60,16 @@ export const LOCAL_APPLY_RECONCILIATION_HANDOFF_FRAME_PATH = "/local-apply/recon
60
60
  export const LOCAL_APPLY_RECONCILIATION_HANDOFF_INPUT_PATH = "/local-apply/reconciliation/handoff/input";
61
61
  export const LOCAL_APPLY_RECONCILIATION_HANDOFF_RESUME_PATH = "/local-apply/reconciliation/handoff/resume";
62
62
  export const LOCAL_APPLY_RECONCILIATION_HANDOFF_END_PATH = "/local-apply/reconciliation/handoff/end";
63
+ /**
64
+ * A sign-in to the job's site in this machine's application browser profile, AFTER a fill ended
65
+ * `blocked` on `login_required` (#1100). Not a run and not a run's handoff: there is no engine, no
66
+ * source, no artifact and no submit gate — only the profile's page at the job's site, for the owner
67
+ * to sign in, and the profile's sign-in ledger (#1094) to record that they did.
68
+ */
69
+ export const LOCAL_APPLY_SIGN_IN_PATH = "/local-apply/sign-in";
70
+ export const LOCAL_APPLY_SIGN_IN_FRAME_PATH = "/local-apply/sign-in/frame";
71
+ export const LOCAL_APPLY_SIGN_IN_INPUT_PATH = "/local-apply/sign-in/input";
72
+ export const LOCAL_APPLY_SIGN_IN_END_PATH = "/local-apply/sign-in/end";
63
73
  export const LOCAL_APPLY_ENGINES = ["claude", "codex"];
64
74
  export const LOCAL_APPLY_AUTH_MODES = ["machine", "subscription"];
65
75
  export const LOCAL_APPLY_MODES = ["fill_and_review", "auto_submit"];
@@ -0,0 +1,218 @@
1
+ /**
2
+ * A post-run site sign-in in this machine's application browser profile (#1100).
3
+ *
4
+ * #1094 made the profile persistent and pauses a fill `login_required` before an uncertain press, so
5
+ * the owner signs in once through the fill's handoff. But that handoff exists only while the fill is
6
+ * paused. The reported run (9a7d9e85) paused at 11:32, nobody reached it before the run's 20-minute
7
+ * limit, and it ended `blocked` at 11:53; the owner asked to take over at 11:57 — when there was no
8
+ * paused fill, no retained page, and so nothing to stream. Every later fill would bounce off the same
9
+ * sign-in, because the only way to sign in had closed with the run.
10
+ *
11
+ * This opens the profile itself at the job's site, with no run: the owner signs in there, and `end`
12
+ * with `complete` records the sign-in in the profile's ledger (names and expiry only) so the next
13
+ * fill's session gate passes. It has no engine, no bridge, no source and no submit gate — the page is
14
+ * for a person, and nothing here fills or presses anything.
15
+ */
16
+ import { join } from "node:path";
17
+ import { RunnerInputError } from "../errors.js";
18
+ import { siteKeyOf } from "./site-session.js";
19
+ const SAFE_ID = /^[A-Za-z0-9_.:-]{1,300}$/;
20
+ /** As long as the durable takeover it serves (`TAKEOVER_TTL_MS` in the Worker). */
21
+ const TTL_MS = 60 * 60_000;
22
+ export class LocalApplySignInRuntime {
23
+ deps;
24
+ sessions = new Map();
25
+ now;
26
+ schedule;
27
+ constructor(deps) {
28
+ this.deps = deps;
29
+ this.now = deps.now ?? (() => Date.now());
30
+ this.schedule = deps.schedule ?? ((task, delayMs) => {
31
+ const timer = setTimeout(task, delayMs);
32
+ return () => clearTimeout(timer);
33
+ });
34
+ }
35
+ /**
36
+ * Open the profile at the job's site, or read back the one already open under this id. Idempotent,
37
+ * so the cloud may call it whenever a frame finds no live page: a page the owner closed is reported
38
+ * closed, and a runner restart (nothing in memory) simply opens it again.
39
+ */
40
+ async open(raw) {
41
+ const request = parseRequest(raw);
42
+ const existing = this.sessions.get(request.signInId);
43
+ if (existing) {
44
+ if (existing.request.browserProfile !== request.browserProfile || existing.request.url !== request.url)
45
+ throw new RunnerInputError("The sign-in id is already bound to another profile or site", 409);
46
+ if (existing.state === "ready" && !(await this.pageLive(existing)))
47
+ await this.close(existing, "page_lost");
48
+ // A finished sign-in is an answer and is read back; one that closed without one (the owner
49
+ // closed the tab, the deadline passed) opens again — the cloud asks only under a live takeover.
50
+ if (existing.state === "ready" || existing.signedIn)
51
+ return this.view(existing);
52
+ this.sessions.delete(request.signInId);
53
+ }
54
+ for (const [id, s] of this.sessions)
55
+ if (s.state === "closed" && s.expiresAt <= this.now())
56
+ this.sessions.delete(id);
57
+ if ([...this.sessions.values()].some((s) => s.state === "ready" && s.request.browserProfile === request.browserProfile))
58
+ throw new RunnerInputError("Another sign-in is already open in this application browser; finish it first.", 409);
59
+ // The `default` profile's page is the runner's own browser page; never navigate it from under a run.
60
+ if (this.deps.busy())
61
+ throw new RunnerInputError("An application run is using this machine's application browser; sign in when it ends.", 409);
62
+ const site = siteKeyOf(new URL(request.url).hostname);
63
+ const takeover = this.deps.takeover;
64
+ let browser;
65
+ try {
66
+ if (!takeover)
67
+ throw new Error("no takeover");
68
+ browser = await this.deps.browserFor(request.browserProfile, join(this.deps.dataDir, "sign-in", request.signInId));
69
+ }
70
+ catch {
71
+ return this.closedView(request, site, "profile_unavailable");
72
+ }
73
+ const session = { request, site, browser, state: "ready", expiresAt: this.now() + TTL_MS };
74
+ this.sessions.set(request.signInId, session);
75
+ session.cancelDeadline = this.schedule(() => void this.close(session, "expired"), TTL_MS);
76
+ const page = this.page(session);
77
+ if (!page) {
78
+ await this.close(session, "profile_unavailable");
79
+ return this.view(session);
80
+ }
81
+ await browser.sessions?.beginSignIn(site).catch(() => undefined);
82
+ // A slow or redirecting site still leaves a page the owner can use; only a lost page is terminal.
83
+ await page.goto(request.url, { waitUntil: "domcontentloaded", timeout: 20_000 }).catch(() => undefined);
84
+ try {
85
+ await takeover.open({ handoffId: request.signInId }, page);
86
+ }
87
+ catch {
88
+ await this.close(session, "page_lost");
89
+ }
90
+ return this.view(session);
91
+ }
92
+ async frame(raw) {
93
+ const session = await this.live(raw);
94
+ try {
95
+ return await this.deps.takeover.frame(session.request.signInId);
96
+ }
97
+ catch {
98
+ await this.close(session, "page_lost");
99
+ throw new RunnerInputError("The sign-in page is no longer available", 409);
100
+ }
101
+ }
102
+ async input(raw) {
103
+ const o = raw && typeof raw === "object" ? raw : {};
104
+ const session = await this.live(o);
105
+ if (!o.input || typeof o.input !== "object")
106
+ throw new RunnerInputError("sign-in input is required");
107
+ try {
108
+ await this.deps.takeover.input(session.request.signInId, o.input);
109
+ }
110
+ catch {
111
+ await this.close(session, "page_lost");
112
+ throw new RunnerInputError("The sign-in page is no longer available", 409);
113
+ }
114
+ }
115
+ /** Close it. `complete: true` is the owner saying they signed in: the ledger records it first. */
116
+ async end(raw) {
117
+ const o = raw && typeof raw === "object" ? raw : {};
118
+ const session = this.require(o);
119
+ if (session.state === "ready" && o.complete === true) {
120
+ const recorded = await session.browser.sessions?.completeSignIn(session.site).catch(() => null);
121
+ if (recorded)
122
+ session.signedIn = recorded;
123
+ }
124
+ await this.close(session, session.signedIn ? undefined : "unavailable");
125
+ return this.view(session);
126
+ }
127
+ liveWork() {
128
+ return [...this.sessions.values()].filter((s) => s.state === "ready").map((s) => `sign-in:${s.request.signInId}`);
129
+ }
130
+ closeAll() {
131
+ for (const session of this.sessions.values())
132
+ void this.close(session, "runner_restarted");
133
+ }
134
+ async close(session, reason) {
135
+ if (session.state === "closed")
136
+ return;
137
+ // Terminal synchronously, so a deadline racing an `end` cannot tear down twice.
138
+ session.state = "closed";
139
+ session.terminalReason = reason;
140
+ session.cancelDeadline?.();
141
+ session.cancelDeadline = undefined;
142
+ await this.deps.takeover?.end(session.request.signInId).catch(() => undefined);
143
+ await session.browser.stop().catch(() => undefined);
144
+ }
145
+ async live(raw) {
146
+ const session = this.require(raw);
147
+ if (session.state === "ready" && session.expiresAt <= this.now())
148
+ await this.close(session, "expired");
149
+ if (session.state === "ready" && !(await this.pageLive(session)))
150
+ await this.close(session, "page_lost");
151
+ if (session.state !== "ready")
152
+ throw new RunnerInputError(`The sign-in is closed (${session.terminalReason ?? "finished"})`, 409);
153
+ return session;
154
+ }
155
+ async pageLive(session) {
156
+ return !!this.page(session) && (await this.deps.takeover?.state(session.request.signInId)) === "ready";
157
+ }
158
+ page(session) {
159
+ const page = session.browser.handoffPage?.();
160
+ return page && !page.isClosed() ? page : null;
161
+ }
162
+ require(raw) {
163
+ const id = raw?.signInId;
164
+ if (typeof id !== "string" || !SAFE_ID.test(id))
165
+ throw new RunnerInputError("signInId is required");
166
+ const session = this.sessions.get(id);
167
+ if (!session)
168
+ throw new RunnerInputError("Sign-in not found", 404);
169
+ return session;
170
+ }
171
+ view(session) {
172
+ return {
173
+ signInId: session.request.signInId,
174
+ browserProfile: session.request.browserProfile,
175
+ site: session.site,
176
+ state: session.state,
177
+ expiresAt: new Date(session.expiresAt).toISOString(),
178
+ ...(session.terminalReason ? { terminalReason: session.terminalReason } : {}),
179
+ ...(session.signedIn ? { signedIn: session.signedIn } : {}),
180
+ };
181
+ }
182
+ closedView(request, site, terminalReason) {
183
+ return { signInId: request.signInId, browserProfile: request.browserProfile, site, state: "closed", expiresAt: new Date(this.now()).toISOString(), terminalReason };
184
+ }
185
+ }
186
+ /** Separate from the fill's handoff and from reconciliation: this surface has no fill or submit operation. */
187
+ export async function routeLocalApplySignIn(runtime, method, path, read) {
188
+ if (method !== "POST")
189
+ return undefined;
190
+ if (path === "/local-apply/sign-in")
191
+ return runtime.open(await read());
192
+ if (path === "/local-apply/sign-in/frame")
193
+ return runtime.frame(await read());
194
+ if (path === "/local-apply/sign-in/input") {
195
+ await runtime.input(await read());
196
+ return { ok: true };
197
+ }
198
+ if (path === "/local-apply/sign-in/end")
199
+ return runtime.end(await read());
200
+ return undefined;
201
+ }
202
+ function parseRequest(raw) {
203
+ const o = raw && typeof raw === "object" ? raw : {};
204
+ const signInId = typeof o.signInId === "string" ? o.signInId : "";
205
+ const browserProfile = o.browserProfile === "default" || o.browserProfile === "isolated" ? o.browserProfile : null;
206
+ const url = typeof o.url === "string" ? o.url : "";
207
+ let ok = false;
208
+ try {
209
+ const u = new URL(url);
210
+ ok = u.protocol === "https:" || u.protocol === "http:";
211
+ }
212
+ catch {
213
+ ok = false;
214
+ }
215
+ if (!SAFE_ID.test(signInId) || !browserProfile || !ok)
216
+ throw new RunnerInputError("sign-in needs an opaque signInId, a browserProfile and an http(s) url");
217
+ return { signInId, browserProfile, url };
218
+ }
@@ -19,6 +19,7 @@ import { LocalArtifactRuntime } from "./local-artifact/runtime.js";
19
19
  import { LOCAL_ARTIFACT_TASK_TYPE } from "./local-artifact/contract.js";
20
20
  import { LocalApplyRuntime } from "./local-apply/runtime.js";
21
21
  import { LocalApplyReconciliationRuntime } from "./local-apply/reconciliation-runtime.js";
22
+ import { LocalApplySignInRuntime } from "./local-apply/sign-in-runtime.js";
22
23
  import { LOCAL_APPLY_TASK_TYPE } from "./local-apply/contract.js";
23
24
  import { createRunnerBrowserFactories } from "./local-apply/live-run-browser.js";
24
25
  import { createLocalApplyTakeoverAdapter, isLocalApplyTakeover } from "./local-apply/takeover-adapter.js";
@@ -95,6 +96,8 @@ export class LocalRunner {
95
96
  localApply;
96
97
  /** Browser-only, no-bridge reconciliation for an ended uncertain apply attempt. */
97
98
  localApplyReconciliation;
99
+ /** A site sign-in in the application profile after a fill ended blocked on it (#1100): no run, no bridge. */
100
+ localApplySignIn;
98
101
  /** The Website Builder's local author (#841): the signed-in CLI drafting through the PAGS broker. */
99
102
  siteBuilder;
100
103
  /** This runner's own local URL, once the server listens — the bridge forwarder calls back to it. */
@@ -118,6 +121,12 @@ export class LocalRunner {
118
121
  takeover: localApplyTakeover,
119
122
  });
120
123
  this.localApplyReconciliation = new LocalApplyReconciliationRuntime({ dataDir: config.dataDir, browserFor: applyBrowserFor, takeover: localApplyTakeover });
124
+ this.localApplySignIn = new LocalApplySignInRuntime({
125
+ dataDir: config.dataDir,
126
+ browserFor: applyBrowserFor,
127
+ takeover: localApplyTakeover,
128
+ busy: () => this.localApply.liveWork().length + this.localApplyReconciliation.liveWork().length + this.localBrowser.liveWork().length > 0,
129
+ });
121
130
  this.siteBuilder = new SiteBuilderRuntime({ dataDir: config.dataDir, selfUrl: () => this.selfUrl, onEvent: (id, type, message, data) => this.addTaskEvent(this.requireTask(id), type, message, data) });
122
131
  // Tasks paused/running on a previous process are orphaned now — their
123
132
  // pages and takeover sessions are gone. Fail them so the board is clean.
@@ -136,7 +145,7 @@ export class LocalRunner {
136
145
  */
137
146
  liveWork() {
138
147
  const coding = this.coding.liveWork();
139
- const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork(), ...this.localApplyReconciliation.liveWork(), ...this.siteBuilder.liveWork()];
148
+ const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork(), ...this.localApplyReconciliation.liveWork(), ...this.localApplySignIn.liveWork(), ...this.siteBuilder.liveWork()];
140
149
  return { codingTurns: coding.length, localRuns: local.length, detail: [...coding, ...local].slice(0, 10) };
141
150
  }
142
151
  capabilities() {
@@ -320,6 +329,7 @@ export class LocalRunner {
320
329
  this.localArtifact.closeAll();
321
330
  this.localApply.closeAll();
322
331
  this.localApplyReconciliation.closeAll();
332
+ this.localApplySignIn.closeAll();
323
333
  try {
324
334
  this.coding.closeAll();
325
335
  }
@@ -8,6 +8,7 @@ import { listGithubOrgs, listGithubRepos, searchGithubRepos, getGithubRepoDetail
8
8
  import { routeLocalApplyHandoff } from "./local-apply/handoff-routes.js";
9
9
  import { runNativePreflight } from "./native/preflight.js";
10
10
  import { routeLocalApplyReconciliation } from "./local-apply/reconciliation-routes.js";
11
+ import { routeLocalApplySignIn } from "./local-apply/sign-in-runtime.js";
11
12
  export function createRunnerServer(runner) {
12
13
  return createServer(async (req, res) => {
13
14
  try {
@@ -273,6 +274,9 @@ async function route(runner, req, res) {
273
274
  const reconciliation = await routeLocalApplyReconciliation(runner.localApplyReconciliation, req.method, path, () => readJson(req));
274
275
  if (reconciliation !== undefined)
275
276
  return json(res, 200, reconciliation);
277
+ const signIn = await routeLocalApplySignIn(runner.localApplySignIn, req.method, path, () => readJson(req));
278
+ if (signIn !== undefined)
279
+ return json(res, 200, signIn);
276
280
  if (req.method === "POST" && path === "/local-apply/resume") {
277
281
  return json(res, 200, runner.localApply.resume(await readJson(req)));
278
282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.133",
3
+ "version": "0.4.134",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",