clay-server 4.7.1-beta.4 → 4.8.0-beta.1
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/bin/login-browser-open.js +7 -0
- package/lib/project-vendor-login.js +52 -18
- package/lib/project.js +3 -0
- package/lib/public/app.js +1 -0
- package/lib/public/css/vendor-login-browser.css +63 -0
- package/lib/public/modules/app-messages.js +5 -0
- package/lib/public/modules/app-notifications.js +1 -1
- package/lib/public/modules/vendor-login-browser.js +243 -0
- package/lib/public/modules/vendor-login.js +23 -4
- package/lib/public/style.css +1 -0
- package/lib/terminal.js +4 -0
- package/lib/vendor-login-browser-flow.js +46 -0
- package/lib/vendor-login-browser-passkeys.js +27 -0
- package/lib/vendor-login-browser-worker.js +149 -0
- package/lib/vendor-login-browser.js +162 -0
- package/lib/ws-schema.js +4 -0
- package/lib/yoke/adapters/codex.js +55 -8
- package/lib/yoke/codex-idle-recovery.js +52 -0
- package/package.json +2 -1
|
@@ -15,6 +15,7 @@ import { getWs } from './ws-ref.js';
|
|
|
15
15
|
import { refreshIcons, iconHtml } from './icons.js';
|
|
16
16
|
import { openTuiModal, closeTuiModal, getTuiModalTerminalId } from './tui-attention.js';
|
|
17
17
|
import { forwardPaneAuthRequired } from './pane-bridge.js';
|
|
18
|
+
import { openLoginBrowser, closeLoginBrowser } from './vendor-login-browser.js';
|
|
18
19
|
|
|
19
20
|
// vendor -> { terminalId, startedAt } as last broadcast by the server.
|
|
20
21
|
var activeFlows = Object.create(null);
|
|
@@ -106,7 +107,7 @@ function showLoginStartedBanner(vendor) {
|
|
|
106
107
|
showLoginBanner({
|
|
107
108
|
icon: "check-circle",
|
|
108
109
|
title: vendorDisplayName(vendor) + " login started",
|
|
109
|
-
text: "Finish
|
|
110
|
+
text: "Finish sign-in in the window below. Clay reconnects your sessions automatically.",
|
|
110
111
|
onDismiss: function () { cancelVendorLogin(vendor); },
|
|
111
112
|
});
|
|
112
113
|
}
|
|
@@ -169,6 +170,7 @@ export function requestVendorLogin(vendor, opts) {
|
|
|
169
170
|
vendor: target,
|
|
170
171
|
auto: !!options.auto,
|
|
171
172
|
sessionId: options.sessionId || store.get('activeSessionId') || null,
|
|
173
|
+
mode: options.mode || ((target === "claude" || target === "codex") ? "browser" : "terminal"),
|
|
172
174
|
}));
|
|
173
175
|
showLoginStartedBanner(target);
|
|
174
176
|
return true;
|
|
@@ -177,13 +179,17 @@ export function requestVendorLogin(vendor, opts) {
|
|
|
177
179
|
export function cancelVendorLogin(vendor) {
|
|
178
180
|
var target = vendor || modalVendor;
|
|
179
181
|
if (!target) return;
|
|
182
|
+
var flow = activeFlows[target];
|
|
183
|
+
var browser = store.get('vendorLoginBrowser');
|
|
184
|
+
var browserId = flow && flow.browserId || (browser && browser.vendor === target ? browser.browserId : null);
|
|
185
|
+
if (browser && browser.vendor === target) closeLoginBrowser();
|
|
180
186
|
delete pendingStarts[target];
|
|
181
187
|
delete activeFlows[target];
|
|
182
188
|
clearLoginBanners();
|
|
183
189
|
if (modalVendor === target) closeModalInternally();
|
|
184
190
|
var ws = getWs();
|
|
185
191
|
if (ws && ws.readyState === 1) {
|
|
186
|
-
ws.send(JSON.stringify({ type: "vendor_login_cancel", vendor: target }));
|
|
192
|
+
ws.send(JSON.stringify({ type: "vendor_login_cancel", vendor: target, browserId: browserId }));
|
|
187
193
|
}
|
|
188
194
|
}
|
|
189
195
|
|
|
@@ -215,7 +221,11 @@ export function handleVendorLoginReady(msg) {
|
|
|
215
221
|
if (!msg || typeof msg.terminalId !== "number") return;
|
|
216
222
|
var vendor = msg.vendor || "claude";
|
|
217
223
|
delete pendingStarts[vendor];
|
|
218
|
-
activeFlows[vendor] = { terminalId: msg.terminalId, startedAt: Date.now() };
|
|
224
|
+
activeFlows[vendor] = { terminalId: msg.terminalId, startedAt: Date.now(), browserId: msg.browserId || null };
|
|
225
|
+
if (msg.browserId) {
|
|
226
|
+
openLoginBrowser(msg);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
219
229
|
|
|
220
230
|
var slug = msg.slug || currentProjectSlug();
|
|
221
231
|
if (!slug) return;
|
|
@@ -239,7 +249,7 @@ export function handleVendorLoginState(msg) {
|
|
|
239
249
|
var next = Object.create(null);
|
|
240
250
|
var flows = msg.flows || [];
|
|
241
251
|
for (var i = 0; i < flows.length; i++) {
|
|
242
|
-
next[flows[i].vendor] = { terminalId: flows[i].terminalId, startedAt: flows[i].startedAt };
|
|
252
|
+
next[flows[i].vendor] = { terminalId: flows[i].terminalId, startedAt: flows[i].startedAt, browserId: flows[i].browserId || null };
|
|
243
253
|
delete pendingStarts[flows[i].vendor];
|
|
244
254
|
}
|
|
245
255
|
|
|
@@ -253,11 +263,18 @@ export function handleVendorLoginState(msg) {
|
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
265
|
activeFlows = next;
|
|
266
|
+
var browser = store.get('vendorLoginBrowser');
|
|
267
|
+
if (browser && (!next[browser.vendor] || next[browser.vendor].browserId !== browser.browserId)) {
|
|
268
|
+
closeLoginBrowser();
|
|
269
|
+
clearLoginBanners();
|
|
270
|
+
}
|
|
256
271
|
}
|
|
257
272
|
|
|
258
273
|
export function handleAuthRefreshed(msg) {
|
|
259
274
|
if (!msg) return;
|
|
260
275
|
var vendor = msg.vendor || "claude";
|
|
276
|
+
var browser = store.get('vendorLoginBrowser');
|
|
277
|
+
if (browser && browser.vendor === vendor) closeLoginBrowser();
|
|
261
278
|
var flow = activeFlows[vendor];
|
|
262
279
|
delete pendingStarts[vendor];
|
|
263
280
|
delete activeFlows[vendor];
|
|
@@ -269,6 +286,8 @@ export function handleAuthRefreshed(msg) {
|
|
|
269
286
|
export function handleVendorLoginError(msg) {
|
|
270
287
|
if (!msg) return;
|
|
271
288
|
var vendor = msg.vendor || "claude";
|
|
289
|
+
var browser = store.get('vendorLoginBrowser');
|
|
290
|
+
if (browser && browser.vendor === vendor) closeLoginBrowser();
|
|
272
291
|
delete pendingStarts[vendor];
|
|
273
292
|
delete activeFlows[vendor];
|
|
274
293
|
if (modalVendor === vendor) closeModalInternally();
|
package/lib/public/style.css
CHANGED
package/lib/terminal.js
CHANGED
|
@@ -25,6 +25,10 @@ function createTerminal(cwd, cols, rows, osUserInfo, opts) {
|
|
|
25
25
|
|
|
26
26
|
// Build a minimal, isolated environment (no daemon env leakage)
|
|
27
27
|
var termEnv = buildUserEnv(osUserInfo);
|
|
28
|
+
if (opts && opts.loginBrowserEnv) {
|
|
29
|
+
termEnv.BROWSER = opts.loginBrowserEnv.BROWSER;
|
|
30
|
+
termEnv.CLAY_LOGIN_URL_FILE = opts.loginBrowserEnv.CLAY_LOGIN_URL_FILE;
|
|
31
|
+
}
|
|
28
32
|
var spawnOpts = {
|
|
29
33
|
name: "xterm-256color",
|
|
30
34
|
cols: cols || 80,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
var createLoginBrowser = require("./vendor-login-browser").createLoginBrowser;
|
|
2
|
+
|
|
3
|
+
function attachBrowserLogin(ctx) {
|
|
4
|
+
function allowed(ws, flow) {
|
|
5
|
+
var userId = ws && ws._clayUser ? String(ws._clayUser.id) : null;
|
|
6
|
+
return userId === flow.ownerId && ctx.hasTerminalPermission(ws);
|
|
7
|
+
}
|
|
8
|
+
function prepare(ws, flow, identity) {
|
|
9
|
+
if (ctx.osUsers && !identity.osUserInfo) throw new Error("OS user identity is unavailable");
|
|
10
|
+
flow.ownerId = ws && ws._clayUser ? String(ws._clayUser.id) : null;
|
|
11
|
+
flow.browser = (ctx.createLoginBrowser || createLoginBrowser)({
|
|
12
|
+
ws: ws, vendor: flow.vendor, osUserInfo: identity.osUserInfo,
|
|
13
|
+
sendTo: ctx.sendTo,
|
|
14
|
+
canAccess: function (peer) { return allowed(peer, flow); },
|
|
15
|
+
});
|
|
16
|
+
flow.expiryTimer = setTimeout(function () { ctx.cancel(flow.key); }, 15 * 60 * 1000);
|
|
17
|
+
flow.expiryTimer.unref();
|
|
18
|
+
}
|
|
19
|
+
function handle(ws, msg, flows) {
|
|
20
|
+
var flow = flows[ctx.flowKey(ws, String(msg.vendor || "claude"))];
|
|
21
|
+
if (msg.type === "vendor_login_cancel" && msg.browserId && (!flow || !flow.browser)) return true;
|
|
22
|
+
if (typeof msg.type === "string" && msg.type.indexOf("term_") === 0) {
|
|
23
|
+
var vendors = Object.keys(flows);
|
|
24
|
+
for (var i = 0; i < vendors.length; i++) {
|
|
25
|
+
var candidate = flows[vendors[i]];
|
|
26
|
+
if (candidate.terminalId === msg.id && !allowed(ws, candidate)) return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (msg.type === "vendor_login_browser_attach" || msg.type === "vendor_login_browser_input") {
|
|
30
|
+
if (!flow || !flow.browser || flow.browser.id !== msg.browserId || !allowed(ws, flow)) return true;
|
|
31
|
+
if (msg.type === "vendor_login_browser_attach") flow.browser.attach(ws);
|
|
32
|
+
else flow.browser.input(ws, msg.event);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (flow && flow.browser && (msg.type === "vendor_login_start" || msg.type === "vendor_login_cancel")) {
|
|
36
|
+
if (!allowed(ws, flow) || (msg.type === "vendor_login_cancel" && msg.browserId !== flow.browser.id)) {
|
|
37
|
+
ctx.sendTo(ws, { type: "vendor_login_error", vendor: msg.vendor, error: "This sign-in belongs to another request. Reopen sign-in to continue." });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return { prepare: prepare, handle: handle };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { attachBrowserLogin: attachBrowserLogin };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Runs before page scripts in each remote sign-in page, frame, and popup.
|
|
2
|
+
function disableLoginPasskeys() {
|
|
3
|
+
var credentials = window.CredentialsContainer && window.CredentialsContainer.prototype;
|
|
4
|
+
if (credentials) {
|
|
5
|
+
["get", "create"].forEach(function (method) {
|
|
6
|
+
var original = credentials[method];
|
|
7
|
+
if (typeof original !== "function") return;
|
|
8
|
+
Object.defineProperty(credentials, method, {
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true,
|
|
11
|
+
value: function (options) {
|
|
12
|
+
if (options && "publicKey" in Object(options)) {
|
|
13
|
+
return Promise.reject(new DOMException(
|
|
14
|
+
"Passkeys are unavailable in this remote sign-in browser. Choose another sign-in method.",
|
|
15
|
+
"NotSupportedError"
|
|
16
|
+
));
|
|
17
|
+
}
|
|
18
|
+
return original.apply(this, arguments);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Feature detection should choose password/email instead of host-native authenticator UI.
|
|
24
|
+
Object.defineProperty(window, "PublicKeyCredential", { configurable: true, writable: true, value: undefined });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { disableLoginPasskeys: disableLoginPasskeys };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Runs as the same OS identity as the login CLI. Only the human supplies input.
|
|
2
|
+
var readline = require("readline");
|
|
3
|
+
var spawn = require("child_process").spawn;
|
|
4
|
+
var chromium = require("playwright").chromium;
|
|
5
|
+
var browser = null;
|
|
6
|
+
var page = null;
|
|
7
|
+
var installing = null;
|
|
8
|
+
var viewing = true;
|
|
9
|
+
var closing = false;
|
|
10
|
+
var captureTimer = null;
|
|
11
|
+
var inputQueue = Promise.resolve();
|
|
12
|
+
var queued = 0;
|
|
13
|
+
var viewport = { width: 1000, height: 720 };
|
|
14
|
+
|
|
15
|
+
function emit(message) {
|
|
16
|
+
if (!closing && process.stdout.writableLength < 1024 * 1024) process.stdout.write(JSON.stringify(message) + "\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function status(text, phase) { emit({ type: "status", text: text, phase: phase || "loading" }); }
|
|
20
|
+
|
|
21
|
+
async function launch() {
|
|
22
|
+
var channels = ["chrome", "msedge", null];
|
|
23
|
+
for (var i = 0; i < channels.length; i++) {
|
|
24
|
+
try {
|
|
25
|
+
return await chromium.launch({ channel: channels[i] || undefined, headless: true, chromiumSandbox: true, timeout: 20000 });
|
|
26
|
+
} catch (e) {
|
|
27
|
+
if (closing) throw e;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
status("Preparing the sign-in browser. The first download may take a few minutes…");
|
|
31
|
+
await new Promise(function (resolve, reject) {
|
|
32
|
+
installing = spawn(process.execPath, [require("path").join(require.resolve("playwright/package.json"), "../cli.js"), "install", "chromium"], { stdio: "ignore", env: process.env });
|
|
33
|
+
var timer = setTimeout(function () { installing.kill(); reject(new Error("Install timed out")); }, 180000);
|
|
34
|
+
installing.once("error", function (err) { clearTimeout(timer); reject(err); });
|
|
35
|
+
installing.once("exit", function (code) {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
installing = null;
|
|
38
|
+
if (code === 0) resolve(); else reject(new Error("Browser installation failed"));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
if (closing) throw new Error("Closed");
|
|
42
|
+
return chromium.launch({ headless: true, chromiumSandbox: true, timeout: 20000 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function capture() {
|
|
46
|
+
if (closing) return;
|
|
47
|
+
try {
|
|
48
|
+
if (page && !page.isClosed() && viewing && process.stdout.writableLength < 262144) {
|
|
49
|
+
var current = page;
|
|
50
|
+
var data = await current.screenshot({ type: "jpeg", quality: 65, caret: "initial", timeout: 3000 });
|
|
51
|
+
if (current === page) emit({ type: "frame", data: data.toString("base64"), origin: new URL(current.url()).origin,
|
|
52
|
+
width: current.viewportSize().width, height: current.viewportSize().height });
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {}
|
|
55
|
+
if (!closing) captureTimer = setTimeout(capture, 250);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function start(url) {
|
|
59
|
+
browser = await launch();
|
|
60
|
+
if (closing) { await browser.close(); return; }
|
|
61
|
+
var session = await browser.newBrowserCDPSession();
|
|
62
|
+
var version;
|
|
63
|
+
try { version = await session.send("Browser.getVersion"); }
|
|
64
|
+
finally { await session.detach(); }
|
|
65
|
+
// Preserve the native version/platform and replace only the headless product token.
|
|
66
|
+
var userAgent = version.userAgent.replace("HeadlessChrome/", "Chrome/");
|
|
67
|
+
var context = await browser.newContext({ viewport: viewport, acceptDownloads: false, userAgent: userAgent });
|
|
68
|
+
await context.addInitScript(require("./vendor-login-browser-passkeys").disableLoginPasskeys);
|
|
69
|
+
var callback = new URL(new URL(url).searchParams.get("redirect_uri"));
|
|
70
|
+
await context.route("**/*", function (route) {
|
|
71
|
+
var target = new URL(route.request().url());
|
|
72
|
+
if (target.protocol === "https:" || (target.origin === callback.origin && target.pathname === callback.pathname)) return route.continue();
|
|
73
|
+
return route.abort();
|
|
74
|
+
});
|
|
75
|
+
context.on("page", function (opened) {
|
|
76
|
+
page = opened;
|
|
77
|
+
opened.setViewportSize(viewport).catch(function () {});
|
|
78
|
+
opened.on("download", function (download) { download.cancel().catch(function () {}); });
|
|
79
|
+
opened.on("close", function () {
|
|
80
|
+
var pages = context.pages();
|
|
81
|
+
page = pages.length ? pages[pages.length - 1] : null;
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
page = await context.newPage();
|
|
85
|
+
capture();
|
|
86
|
+
status("Opening sign-in page…");
|
|
87
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60000 });
|
|
88
|
+
status("Sign in to connect your account.", "ready");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function input(event) {
|
|
92
|
+
if (!page || page.isClosed() || closing) return;
|
|
93
|
+
if (["move", "down", "up"].indexOf(event.kind) >= 0 && Number.isFinite(event.x) && Number.isFinite(event.y)) {
|
|
94
|
+
var pointerSize = page.viewportSize();
|
|
95
|
+
var x = Math.max(0, Math.min(pointerSize.width - 1, event.x));
|
|
96
|
+
var y = Math.max(0, Math.min(pointerSize.height - 1, event.y));
|
|
97
|
+
await page.mouse.move(x, y);
|
|
98
|
+
if (event.kind === "down") await page.mouse.down();
|
|
99
|
+
if (event.kind === "up") await page.mouse.up();
|
|
100
|
+
} else if (event.kind === "click" && Number.isFinite(event.x) && Number.isFinite(event.y)) {
|
|
101
|
+
var size = page.viewportSize();
|
|
102
|
+
await page.mouse.click(Math.max(0, Math.min(size.width - 1, event.x)), Math.max(0, Math.min(size.height - 1, event.y)));
|
|
103
|
+
} else if (event.kind === "wheel" && Number.isFinite(event.deltaY)) {
|
|
104
|
+
await page.mouse.wheel(0, Math.max(-1500, Math.min(1500, event.deltaY)));
|
|
105
|
+
} else if (event.kind === "text" && typeof event.text === "string" && event.text.length <= 4096) {
|
|
106
|
+
await page.keyboard.insertText(event.text);
|
|
107
|
+
} else if (event.kind === "key" && /^(Tab|Shift\+Tab|Enter|Backspace|Delete|Escape|ArrowLeft|ArrowRight|ArrowUp|ArrowDown|Home|End|ControlOrMeta\+A)$/.test(event.key)) {
|
|
108
|
+
await page.keyboard.press(event.key);
|
|
109
|
+
} else if (event.kind === "reload") {
|
|
110
|
+
await page.reload({ waitUntil: "domcontentloaded", timeout: 30000 });
|
|
111
|
+
} else if (event.kind === "resize" && Number.isFinite(event.width) && Number.isFinite(event.height)) {
|
|
112
|
+
viewport = { width: Math.round(Math.max(320, Math.min(1000, event.width))), height: Math.round(Math.max(360, Math.min(720, event.height))) };
|
|
113
|
+
await page.setViewportSize(viewport);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function close() {
|
|
118
|
+
if (closing) return;
|
|
119
|
+
closing = true;
|
|
120
|
+
clearTimeout(captureTimer);
|
|
121
|
+
if (installing) installing.kill();
|
|
122
|
+
if (browser) await browser.close().catch(function () {});
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var started = false;
|
|
127
|
+
var lines = readline.createInterface({ input: process.stdin });
|
|
128
|
+
lines.on("line", function (line) {
|
|
129
|
+
if (line.length > 20000 || closing) return;
|
|
130
|
+
var message;
|
|
131
|
+
try { message = JSON.parse(line); } catch (e) { return; }
|
|
132
|
+
if (message.type === "start" && !started) {
|
|
133
|
+
started = true;
|
|
134
|
+
start(message.url).catch(function () {
|
|
135
|
+
status("Unable to open the sign-in browser. The host may need browser libraries or sandbox support. Use terminal sign-in to continue.", "error");
|
|
136
|
+
});
|
|
137
|
+
} else if (message.type === "viewing") {
|
|
138
|
+
viewing = !!message.value;
|
|
139
|
+
} else if (message.type === "input" && message.event && queued < 100) {
|
|
140
|
+
if (message.event.kind === "move" && queued >= 10) return;
|
|
141
|
+
queued++;
|
|
142
|
+
inputQueue = inputQueue.then(function () { return input(message.event); }).catch(function () {
|
|
143
|
+
status("The page did not respond. Try reloading.", "error");
|
|
144
|
+
}).finally(function () { queued--; });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
lines.on("close", close);
|
|
148
|
+
process.on("SIGTERM", close);
|
|
149
|
+
process.on("SIGINT", close);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Private, human-controlled browser transport for a single vendor login.
|
|
2
|
+
var fs = require("fs");
|
|
3
|
+
var os = require("os");
|
|
4
|
+
var path = require("path");
|
|
5
|
+
var crypto = require("crypto");
|
|
6
|
+
var spawn = require("child_process").spawn;
|
|
7
|
+
var buildUserEnv = require("./build-user-env").buildUserEnv;
|
|
8
|
+
var wrapSpawnAsUser = require("./os-users").wrapSpawnAsUser;
|
|
9
|
+
|
|
10
|
+
function loginUrl(value, vendor) {
|
|
11
|
+
try {
|
|
12
|
+
var url = new URL(value);
|
|
13
|
+
var hosts = vendor === "codex" ? ["auth.openai.com"] : ["claude.ai", "claude.com", "console.anthropic.com", "platform.claude.com"];
|
|
14
|
+
if (url.protocol !== "https:" || url.username || url.password || hosts.indexOf(url.hostname) < 0) return null;
|
|
15
|
+
if (!url.searchParams.get("state") || !url.searchParams.get("code_challenge")) return null;
|
|
16
|
+
var callback = new URL(url.searchParams.get("redirect_uri"));
|
|
17
|
+
if (callback.protocol !== "http:" || ["localhost", "127.0.0.1", "[::1]"].indexOf(callback.hostname) < 0) return null;
|
|
18
|
+
return url.href;
|
|
19
|
+
} catch (e) { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createLoginBrowser(options) {
|
|
23
|
+
var id = crypto.randomUUID();
|
|
24
|
+
var dir = fs.mkdtempSync(path.join(os.tmpdir(), "clay-login-"));
|
|
25
|
+
fs.chmodSync(dir, 0o700);
|
|
26
|
+
var info = options.osUserInfo;
|
|
27
|
+
if (info) fs.chownSync(dir, info.uid, info.gid);
|
|
28
|
+
var urlFile = path.join(dir, "url");
|
|
29
|
+
var opener = path.resolve(__dirname, "../bin/login-browser-open.js");
|
|
30
|
+
var wrapper = path.join(dir, process.platform === "win32" ? "open.cmd" : "open");
|
|
31
|
+
var quote = function (value) { return "'" + value.replace(/'/g, "'\\''") + "'"; };
|
|
32
|
+
var script = process.platform === "win32"
|
|
33
|
+
? '@"' + process.execPath + '" "' + opener + '" %*\r\n'
|
|
34
|
+
: "#!/bin/sh\nexec " + quote(process.execPath) + " " + quote(opener) + ' "$@"\n';
|
|
35
|
+
fs.writeFileSync(wrapper, script, { mode: 0o700 });
|
|
36
|
+
if (info) fs.chownSync(wrapper, info.uid, info.gid);
|
|
37
|
+
var child = null;
|
|
38
|
+
var peer = options.ws;
|
|
39
|
+
var closed = false;
|
|
40
|
+
var started = false;
|
|
41
|
+
var status = "Waiting for the sign-in page…";
|
|
42
|
+
var statusPhase = "loading";
|
|
43
|
+
var tail = "";
|
|
44
|
+
var pollBusy = false;
|
|
45
|
+
var waitTimer = setTimeout(function () {
|
|
46
|
+
if (!started && !closed) {
|
|
47
|
+
status = "Still waiting for the CLI sign-in page. Use terminal sign-in if it needs attention.";
|
|
48
|
+
send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
|
|
49
|
+
}
|
|
50
|
+
}, 30000);
|
|
51
|
+
waitTimer.unref();
|
|
52
|
+
|
|
53
|
+
function send(message) {
|
|
54
|
+
if (closed || !peer || peer.readyState !== 1 || !options.canAccess(peer)) return;
|
|
55
|
+
if (message.type === "vendor_login_browser_frame" && peer.bufferedAmount > 1024 * 1024) return;
|
|
56
|
+
options.sendTo(peer, Object.assign({ vendor: options.vendor, browserId: id }, message));
|
|
57
|
+
}
|
|
58
|
+
function write(message) {
|
|
59
|
+
if (child && child.stdin.writable && child.stdin.writableLength < 65536) child.stdin.write(JSON.stringify(message) + "\n");
|
|
60
|
+
}
|
|
61
|
+
function start(url) {
|
|
62
|
+
if (closed || started) return;
|
|
63
|
+
started = true;
|
|
64
|
+
clearTimeout(waitTimer);
|
|
65
|
+
status = "Opening a private sign-in browser…";
|
|
66
|
+
send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
|
|
67
|
+
var env = buildUserEnv(info);
|
|
68
|
+
if (process.platform === "win32" && !info) {
|
|
69
|
+
["SystemRoot", "WINDIR", "TEMP", "TMP", "LOCALAPPDATA", "APPDATA", "USERPROFILE", "COMSPEC", "PATHEXT"].forEach(function (key) {
|
|
70
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
var opts = { cwd: info ? info.home : os.homedir(), env: env, stdio: ["pipe", "pipe", "ignore"] };
|
|
74
|
+
if (info) { opts.uid = info.uid; opts.gid = info.gid; }
|
|
75
|
+
var spec = wrapSpawnAsUser(process.execPath, [path.join(__dirname, "vendor-login-browser-worker.js")], opts);
|
|
76
|
+
child = spawn(spec.command, spec.args, spec.options);
|
|
77
|
+
var buffer = "";
|
|
78
|
+
child.stdout.setEncoding("utf8");
|
|
79
|
+
child.stdout.on("data", function (chunk) {
|
|
80
|
+
buffer += chunk;
|
|
81
|
+
if (buffer.length > 4 * 1024 * 1024) { child.kill(); return; }
|
|
82
|
+
var at;
|
|
83
|
+
while ((at = buffer.indexOf("\n")) >= 0) {
|
|
84
|
+
var line = buffer.slice(0, at);
|
|
85
|
+
buffer = buffer.slice(at + 1);
|
|
86
|
+
try {
|
|
87
|
+
var message = JSON.parse(line);
|
|
88
|
+
if (message.type === "status") {
|
|
89
|
+
status = message.text;
|
|
90
|
+
statusPhase = message.phase || "loading";
|
|
91
|
+
send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
|
|
92
|
+
} else if (message.type === "frame") {
|
|
93
|
+
send({ type: "vendor_login_browser_frame", data: message.data, origin: message.origin, width: message.width, height: message.height });
|
|
94
|
+
write({ type: "viewing", value: !!(peer && peer.readyState === 1 && options.canAccess(peer)) });
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
child.on("error", function () {
|
|
100
|
+
statusPhase = "error";
|
|
101
|
+
status = "The sign-in browser could not start. Use terminal sign-in to continue.";
|
|
102
|
+
send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
|
|
103
|
+
});
|
|
104
|
+
child.on("exit", function () {
|
|
105
|
+
if (closed) return;
|
|
106
|
+
statusPhase = "error";
|
|
107
|
+
status = "The sign-in browser closed. You can retry or use terminal sign-in.";
|
|
108
|
+
send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
|
|
109
|
+
});
|
|
110
|
+
child.stdin.on("error", function () {});
|
|
111
|
+
write({ type: "start", url: url });
|
|
112
|
+
}
|
|
113
|
+
function acceptUrl(value) {
|
|
114
|
+
var url = loginUrl(value, options.vendor);
|
|
115
|
+
if (url) start(url);
|
|
116
|
+
}
|
|
117
|
+
var timer = setInterval(function () {
|
|
118
|
+
if (closed || started || pollBusy) return;
|
|
119
|
+
pollBusy = true;
|
|
120
|
+
fs.readFile(urlFile, "utf8", function (err, value) {
|
|
121
|
+
pollBusy = false;
|
|
122
|
+
if (!err && value.length <= 16384) acceptUrl(value);
|
|
123
|
+
});
|
|
124
|
+
}, 250);
|
|
125
|
+
timer.unref();
|
|
126
|
+
return {
|
|
127
|
+
id: id,
|
|
128
|
+
env: { BROWSER: wrapper, CLAY_LOGIN_URL_FILE: urlFile },
|
|
129
|
+
output: function (chunk) {
|
|
130
|
+
// Codex also prints the complete URL when the platform opener ignores BROWSER.
|
|
131
|
+
tail = (tail + chunk).slice(-32768);
|
|
132
|
+
var matches = tail.match(/https:\/\/[^\s\x1b<>"']+(?=[\s\x1b<>"'])/g) || [];
|
|
133
|
+
for (var i = 0; i < matches.length; i++) acceptUrl(matches[i]);
|
|
134
|
+
},
|
|
135
|
+
attach: function (ws) {
|
|
136
|
+
peer = ws;
|
|
137
|
+
send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
|
|
138
|
+
write({ type: "viewing", value: true });
|
|
139
|
+
},
|
|
140
|
+
input: function (ws, event) {
|
|
141
|
+
if (ws !== peer || !options.canAccess(ws) || !event || typeof event !== "object") return;
|
|
142
|
+
write({ type: "input", event: event });
|
|
143
|
+
},
|
|
144
|
+
close: function () {
|
|
145
|
+
if (closed) return;
|
|
146
|
+
closed = true;
|
|
147
|
+
clearInterval(timer);
|
|
148
|
+
clearTimeout(waitTimer);
|
|
149
|
+
if (child) {
|
|
150
|
+
child.stdin.end();
|
|
151
|
+
var killTimer = setTimeout(function () { child.kill("SIGKILL"); }, 5000);
|
|
152
|
+
killTimer.unref();
|
|
153
|
+
child.once("exit", function () { clearTimeout(killTimer); });
|
|
154
|
+
}
|
|
155
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
tail = "";
|
|
157
|
+
peer = null;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { createLoginBrowser: createLoginBrowser, loginUrl: loginUrl };
|
package/lib/ws-schema.js
CHANGED
|
@@ -220,6 +220,10 @@ var schema = {
|
|
|
220
220
|
// -----------------------------------------------------------------------
|
|
221
221
|
"auth_required": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Authentication required (e.g. API key needed)" },
|
|
222
222
|
"vendor_login_start": { direction: "c2s", handler: "lib/project-vendor-login.js", description: "Start (or re-attach to) the per-vendor login terminal" },
|
|
223
|
+
"vendor_login_browser_attach": { direction: "c2s", handler: "lib/vendor-login-browser-flow.js", description: "Attach the sign-in owner to their private browser" },
|
|
224
|
+
"vendor_login_browser_input": { direction: "c2s", handler: "lib/vendor-login-browser-flow.js", description: "Send human input to the private sign-in browser" },
|
|
225
|
+
"vendor_login_browser_frame": { direction: "s2c", handler: "lib/public/modules/vendor-login-browser.js", description: "Private sign-in viewport; never recorded in conversation history" },
|
|
226
|
+
"vendor_login_browser_status": { direction: "s2c", handler: "lib/public/modules/vendor-login-browser.js", description: "Private sign-in browser status" },
|
|
223
227
|
"vendor_login_cancel": { direction: "c2s", handler: "lib/project-vendor-login.js", description: "Dismiss the login flow and kill its terminal" },
|
|
224
228
|
"vendor_login_state_request": { direction: "c2s", handler: "lib/project-vendor-login.js", description: "Ask for the project's active login flows" },
|
|
225
229
|
"vendor_login_ready": { direction: "s2c", handler: "lib/public/modules/vendor-login.js", description: "Login terminal is available; open/re-attach the modal to it" },
|
|
@@ -14,6 +14,7 @@ var { resolveOsUserInfo } = require("../../os-users");
|
|
|
14
14
|
var backgroundTasks = require("../codex-background-tasks");
|
|
15
15
|
var codexUserInput = require("../codex-user-input");
|
|
16
16
|
var createTurnStop = require("../codex-turn-stop").createTurnStop;
|
|
17
|
+
var idleRecovery = require("../codex-idle-recovery");
|
|
17
18
|
|
|
18
19
|
// --- Reasoning defaults ---
|
|
19
20
|
// Reasoning summaries are the only readable thinking channel Codex offers:
|
|
@@ -895,6 +896,9 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
895
896
|
var messageWaiting = null;
|
|
896
897
|
var messageQueueEnded = false;
|
|
897
898
|
var handlerEntry = null;
|
|
899
|
+
var inputRevision = 0;
|
|
900
|
+
var resolveLoopFinished;
|
|
901
|
+
var loopFinished = new Promise(function(resolve) { resolveLoopFinished = resolve; });
|
|
898
902
|
|
|
899
903
|
function pushMessageToQueue(msg) {
|
|
900
904
|
if (messageQueueEnded) return false;
|
|
@@ -1314,6 +1318,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1314
1318
|
|
|
1315
1319
|
state.done = true;
|
|
1316
1320
|
endIterator();
|
|
1321
|
+
resolveLoopFinished();
|
|
1317
1322
|
}
|
|
1318
1323
|
|
|
1319
1324
|
var handle = {
|
|
@@ -1333,6 +1338,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1333
1338
|
|
|
1334
1339
|
pushMessage: function(text, images) {
|
|
1335
1340
|
if (iteratorDone || state.done || messageQueueEnded) return false;
|
|
1341
|
+
inputRevision++;
|
|
1336
1342
|
var input;
|
|
1337
1343
|
if (images && images.length > 0) {
|
|
1338
1344
|
input = [];
|
|
@@ -1354,6 +1360,16 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1354
1360
|
}
|
|
1355
1361
|
},
|
|
1356
1362
|
|
|
1363
|
+
getIdleRecoverySnapshot: function() {
|
|
1364
|
+
if (!state.threadId || !messageWaiting || messageQueue.length || messageQueueEnded || iteratorDone || isCancelled()) return null;
|
|
1365
|
+
return { threadId: state.threadId, revision: inputRevision };
|
|
1366
|
+
},
|
|
1367
|
+
|
|
1368
|
+
retireIdleForRecovery: function() {
|
|
1369
|
+
handle.endInput();
|
|
1370
|
+
return loopFinished;
|
|
1371
|
+
},
|
|
1372
|
+
|
|
1357
1373
|
setModel: function(model) {
|
|
1358
1374
|
state.model = model || DEFAULT_CODEX_MODEL;
|
|
1359
1375
|
return Promise.resolve();
|
|
@@ -1477,6 +1493,7 @@ function createCodexAdapter(opts) {
|
|
|
1477
1493
|
var _initPromise = null;
|
|
1478
1494
|
var _initializing = false;
|
|
1479
1495
|
var _shutdownPromise = null;
|
|
1496
|
+
var _shutdownEpoch = 0;
|
|
1480
1497
|
var _refCount = 0;
|
|
1481
1498
|
var _lastActiveAt = Date.now();
|
|
1482
1499
|
var _shuttingDown = false;
|
|
@@ -1560,10 +1577,6 @@ function createCodexAdapter(opts) {
|
|
|
1560
1577
|
updateLastActiveAt();
|
|
1561
1578
|
}
|
|
1562
1579
|
|
|
1563
|
-
function waitForRecoveryHandoff() {
|
|
1564
|
-
return _recoveryHandoff || Promise.resolve();
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
1580
|
function retainRecoveryRuntime(runtime) {
|
|
1568
1581
|
_recoveryRuntimes.push(runtime);
|
|
1569
1582
|
}
|
|
@@ -1612,6 +1625,11 @@ function createCodexAdapter(opts) {
|
|
|
1612
1625
|
}
|
|
1613
1626
|
|
|
1614
1627
|
async function createIsolatedRecoveryQuery(queryOpts) {
|
|
1628
|
+
var shutdownEpoch = _shutdownEpoch;
|
|
1629
|
+
function checkCurrent() {
|
|
1630
|
+
idleRecovery.checkAbort(queryOpts);
|
|
1631
|
+
if (_shuttingDown || shutdownEpoch !== _shutdownEpoch) throw createShutdownError();
|
|
1632
|
+
}
|
|
1615
1633
|
var releaseHandoff;
|
|
1616
1634
|
var priorHandoff = _recoveryHandoff;
|
|
1617
1635
|
var myHandoff = new Promise(function(resolve) { releaseHandoff = resolve; });
|
|
@@ -1619,6 +1637,11 @@ function createCodexAdapter(opts) {
|
|
|
1619
1637
|
await (priorHandoff || Promise.resolve());
|
|
1620
1638
|
try {
|
|
1621
1639
|
await _recoveryCleanup;
|
|
1640
|
+
checkCurrent();
|
|
1641
|
+
if (!_initializing && _refCount > 0) {
|
|
1642
|
+
await idleRecovery.retireIdleQueries(_activeQueries.slice(), _appServer, checkCurrent);
|
|
1643
|
+
}
|
|
1644
|
+
checkCurrent();
|
|
1622
1645
|
if (_refCount > 0 || _initializing) {
|
|
1623
1646
|
throw new Error("Codex resumed-tool recovery is waiting for an active shared runtime; retry after its current queries finish.");
|
|
1624
1647
|
}
|
|
@@ -1626,6 +1649,7 @@ function createCodexAdapter(opts) {
|
|
|
1626
1649
|
var stopped = await adapter.shutdownIfIdle(0);
|
|
1627
1650
|
if (!stopped) throw new Error("Codex resumed-tool recovery could not safely release the shared runtime; retry after its current queries finish.");
|
|
1628
1651
|
}
|
|
1652
|
+
checkCurrent();
|
|
1629
1653
|
var recoveryRuntime = createCodexAdapter(Object.assign({}, _defaultInitOpts, {
|
|
1630
1654
|
recoveryRuntime: true,
|
|
1631
1655
|
recoverySessionMcpServer: queryOpts.sessionMcpServer,
|
|
@@ -1792,6 +1816,13 @@ function createCodexAdapter(opts) {
|
|
|
1792
1816
|
if (_shuttingDown) {
|
|
1793
1817
|
return Promise.reject(createShutdownError());
|
|
1794
1818
|
}
|
|
1819
|
+
if (_recoveryHandoff) {
|
|
1820
|
+
var initEpoch = _shutdownEpoch;
|
|
1821
|
+
return _recoveryHandoff.then(function() {
|
|
1822
|
+
if (initEpoch !== _shutdownEpoch) throw createShutdownError();
|
|
1823
|
+
return adapter.init(initOpts);
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1795
1826
|
|
|
1796
1827
|
var effectiveInitOpts = Object.assign({}, _defaultInitOpts, initOpts || {});
|
|
1797
1828
|
|
|
@@ -2004,10 +2035,25 @@ function createCodexAdapter(opts) {
|
|
|
2004
2035
|
if (queryOpts && queryOpts.sessionMcpServer && queryOpts.resumeSessionId && !queryOpts.isolatedRecovery) {
|
|
2005
2036
|
return createIsolatedRecoveryQuery(queryOpts);
|
|
2006
2037
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2038
|
+
var queryEpoch = _shutdownEpoch;
|
|
2039
|
+
// Recheck after every await: recovery may replace the runtime while an
|
|
2040
|
+
// ordinary start is waiting for initialization or an earlier handoff.
|
|
2041
|
+
while (true) {
|
|
2042
|
+
idleRecovery.checkAbort(queryOpts || {});
|
|
2043
|
+
if (_shuttingDown || queryEpoch !== _shutdownEpoch) throw createShutdownError();
|
|
2044
|
+
if (_recoveryHandoff) {
|
|
2045
|
+
await _recoveryHandoff;
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
if (_initializing && _initPromise) {
|
|
2049
|
+
await _initPromise;
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
if (!_appServer || !_appServer.started) {
|
|
2053
|
+
await adapter.init(queryOpts || {});
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
break;
|
|
2011
2057
|
}
|
|
2012
2058
|
|
|
2013
2059
|
if (_shuttingDown) {
|
|
@@ -2186,6 +2232,7 @@ function createCodexAdapter(opts) {
|
|
|
2186
2232
|
|
|
2187
2233
|
// Shutdown the app-server process
|
|
2188
2234
|
shutdown: function() {
|
|
2235
|
+
_shutdownEpoch++;
|
|
2189
2236
|
return Promise.all([
|
|
2190
2237
|
beginShutdown(true),
|
|
2191
2238
|
shutdownUserRuntimes("shutdown"),
|