clay-server 5.5.0-beta.2 → 5.5.0-beta.4
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/lib/clay-product-control-input.js +2 -2
- package/lib/clay-product-control-tools.js +1 -1
- package/lib/clay-product-control.js +4 -3
- package/lib/daemon-project-access.js +10 -2
- package/lib/daemon-projects.js +16 -4
- package/lib/daemon-worktree-controls.js +171 -0
- package/lib/daemon.js +14 -63
- package/lib/personal-provider-install.js +85 -0
- package/lib/personal-provider-settings.js +94 -0
- package/lib/project-logs-service.js +2 -2
- package/lib/project-pair-task-control.js +1 -1
- package/lib/project-pending-message-queue.js +6 -2
- package/lib/project-product-control.js +1 -0
- package/lib/project-session-actions.js +46 -3
- package/lib/project-session-pair.js +2 -0
- package/lib/project-sessions.js +11 -3
- package/lib/project-user-message.js +5 -1
- package/lib/project-vendor-login.js +95 -32
- package/lib/project-worker-auth.js +71 -0
- package/lib/project-worker-conversation.js +11 -2
- package/lib/project.js +9 -0
- package/lib/public/app.js +22 -0
- package/lib/public/css/icon-strip.css +5 -0
- package/lib/public/css/overlays.css +18 -0
- package/lib/public/css/settings-shell.css +13 -0
- package/lib/public/css/settings-ui.css +9 -0
- package/lib/public/css/split-view-layout.css +16 -11
- package/lib/public/modules/agent-behavior-nav.js +1 -0
- package/lib/public/modules/agent-message-bubbles.js +33 -4
- package/lib/public/modules/app-messages.js +7 -1
- package/lib/public/modules/app-projects.js +3 -4
- package/lib/public/modules/clay-control-copy.js +1 -0
- package/lib/public/modules/clay-tool-presentation.js +1 -1
- package/lib/public/modules/generation-stop.js +4 -4
- package/lib/public/modules/pane-activation.js +2 -2
- package/lib/public/modules/pane-bridge.js +4 -2
- package/lib/public/modules/personal-provider-context.js +11 -0
- package/lib/public/modules/project-settings.js +1 -1
- package/lib/public/modules/settings-section-nav.js +58 -0
- package/lib/public/modules/settings-shell-registry.js +7 -4
- package/lib/public/modules/settings-shell.js +17 -6
- package/lib/public/modules/sidebar-projects.js +23 -9
- package/lib/public/modules/split-pane-force-stop.js +47 -0
- package/lib/public/modules/split-pane-header-state.js +11 -1
- package/lib/public/modules/split-pane-renderer.js +2 -2
- package/lib/public/modules/split-view-workers.js +32 -17
- package/lib/public/modules/stt-settings.js +21 -4
- package/lib/public/modules/user-settings-layout.js +22 -18
- package/lib/public/modules/user-settings-provider-install.js +42 -0
- package/lib/public/modules/user-settings-providers.js +102 -0
- package/lib/public/modules/user-settings-voice.js +50 -41
- package/lib/public/modules/vendor-login-browser.js +1 -1
- package/lib/public/modules/vendor-login.js +72 -12
- package/lib/public/modules/worker-activity-indicator.js +49 -0
- package/lib/public/modules/worker-message-previews.js +47 -11
- package/lib/public/modules/worker-stop-controls.js +36 -0
- package/lib/public/modules/worktree-controls.js +86 -0
- package/lib/public/pane-assets/{app-SEMVZ4AY.js → app-PV3SEOTX.js} +1 -1
- package/lib/public/pane-assets/{chunk-C6P7PICY.js → chunk-2G6DB3ES.js} +1 -1
- package/lib/public/pane-assets/{chunk-BNNPJJVG.js → chunk-4WYURTPH.js} +1 -1
- package/lib/public/pane-assets/{chunk-E4VM2IDB.js → chunk-53KRXJDL.js} +1 -1
- package/lib/public/pane-assets/{chunk-4YE4RU27.js → chunk-AMZ7P526.js} +1 -1
- package/lib/public/pane-assets/chunk-BK2NRZFM.js +1 -0
- package/lib/public/pane-assets/{chunk-5UBN6GR7.js → chunk-D3WR2TYZ.js} +1 -1
- package/lib/public/pane-assets/chunk-E7D5A27I.js +1 -0
- package/lib/public/pane-assets/{chunk-CQNPEH2M.js → chunk-FB5B2WYF.js} +1 -1
- package/lib/public/pane-assets/chunk-FDBSE7BE.js +174 -0
- package/lib/public/pane-assets/{chunk-FK3YLGAP.js → chunk-FHB76KH2.js} +1 -1
- package/lib/public/pane-assets/{chunk-YR5GH3GI.js → chunk-FNGO6432.js} +1 -1
- package/lib/public/pane-assets/chunk-HF7AK56Q.js +1 -0
- package/lib/public/pane-assets/chunk-ILVRGTDF.js +1 -0
- package/lib/public/pane-assets/{chunk-QWQB2YRF.js → chunk-L3SFMSLP.js} +1 -1
- package/lib/public/pane-assets/{chunk-QBCM7QEA.js → chunk-PAY3A4JS.js} +1 -1
- package/lib/public/pane-assets/chunk-QVHC324H.js +1 -0
- package/lib/public/pane-assets/chunk-SFANEWZA.js +1 -0
- package/lib/public/pane-assets/{chunk-M5CWIIIT.js → chunk-SGMEO3P7.js} +1 -1
- package/lib/public/pane-assets/{chunk-YVVT3CTV.js → chunk-TK446XFF.js} +1 -1
- package/lib/public/pane-assets/{chunk-U7EHYRJZ.js → chunk-TU3AHPSN.js} +1 -1
- package/lib/public/pane-assets/chunk-US5VELGB.js +1 -0
- package/lib/public/pane-assets/{chunk-K4PAN77B.js → chunk-UVTS6GUM.js} +1 -1
- package/lib/public/pane-assets/{chunk-XYZGVYWW.js → chunk-X45WI2U7.js} +1 -1
- package/lib/public/pane-assets/manifest.json +401 -393
- package/lib/public/pane-assets/style-563D4UN6.css +1 -0
- package/lib/sdk-bridge.js +40 -6
- package/lib/sdk-force-stop.js +62 -0
- package/lib/sdk-message-processor.js +2 -1
- package/lib/server-worktree-controls.js +34 -0
- package/lib/server.js +11 -1
- package/lib/session-force-stop-support.js +15 -0
- package/lib/sessions.js +6 -1
- package/lib/terminal-manager.js +1 -1
- package/lib/terminal.js +13 -1
- package/lib/vendor-login-browser-flow.js +1 -1
- package/lib/worker-activity.js +26 -0
- package/lib/worker-message-previews.js +12 -7
- package/lib/worktree-branch.js +57 -0
- package/lib/worktree-identity.js +39 -0
- package/lib/worktree-preferences.js +25 -0
- package/lib/worktree-ref-transaction.js +36 -0
- package/lib/yoke/adapters/antigravity.js +3 -0
- package/lib/yoke/force-termination.js +26 -0
- package/lib/yoke/interface.js +14 -0
- package/lib/yoke/process-force-termination.js +24 -0
- package/lib/yoke/provider-install-recipes.js +20 -0
- package/lib/yoke/vendor-registry.js +20 -1
- package/package.json +1 -1
- package/lib/public/pane-assets/chunk-6AGSOAOD.js +0 -1
- package/lib/public/pane-assets/chunk-AATAIAXL.js +0 -1
- package/lib/public/pane-assets/chunk-D3CE5FZL.js +0 -1
- package/lib/public/pane-assets/chunk-GKSLBGVE.js +0 -168
- package/lib/public/pane-assets/chunk-J5PTSKJ6.js +0 -1
- package/lib/public/pane-assets/chunk-QPBO6ID4.js +0 -1
- package/lib/public/pane-assets/chunk-RA6VUJTY.js +0 -1
- package/lib/public/pane-assets/chunk-XOZUM5ET.js +0 -1
- package/lib/public/pane-assets/style-DZX6JAMF.css +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Personal provider discovery and sign-in authority. No credential files are read.
|
|
2
|
+
var crypto = require('crypto');
|
|
3
|
+
var os = require('os');
|
|
4
|
+
var registry = require('./yoke/vendor-registry');
|
|
5
|
+
var protocol = require('./yoke/login-protocol');
|
|
6
|
+
var installRecipes = require('./yoke/provider-install-recipes');
|
|
7
|
+
var LIFETIME = 5 * 60 * 1000;
|
|
8
|
+
function attach(ctx) {
|
|
9
|
+
var receipts = new WeakMap();
|
|
10
|
+
function actor(ws) {
|
|
11
|
+
if (!ws || ws.readyState !== 1 || ws._clayPaneActivation || ws._clayPane || ctx.clients && !ctx.clients.has(ws)) throw new Error('Open Personal settings in the connected workspace.');
|
|
12
|
+
if (ctx.requestAccess && !ctx.requestAccess.canAccessProject(ws, ctx.slug)) throw new Error('Project access changed. Reopen Personal settings.');
|
|
13
|
+
var users = ctx.usersModule;
|
|
14
|
+
var user = ws._clayUser;
|
|
15
|
+
if (users && users.isMultiUser() && (!user || !users.findUserById(user.id))) throw new Error('Sign in again to manage providers.');
|
|
16
|
+
if (user && users && users.findUserById) user = users.findUserById(user.id);
|
|
17
|
+
if (ws._clayUser && !user) throw new Error('This account is no longer available.');
|
|
18
|
+
if (ctx.osUsers && (!user || !user.linuxUser)) throw new Error('Your mapped OS identity is unavailable.');
|
|
19
|
+
var info = ctx.osUsers ? (ctx.resolveOsUserInfo ? ctx.resolveOsUserInfo(user.linuxUser) : ctx.getOsUserInfoForWs({ _clayUser: user })) : null;
|
|
20
|
+
if (ctx.osUsers && (!info || !info.user || !info.home)) throw new Error('Your mapped OS identity is unavailable. Ask the server administrator to check the mapping.');
|
|
21
|
+
return { accountName:user ? user.displayName || user.username || String(user.id) : 'Personal account', role: user ? user.role : 'admin', runtime: info || {user:os.userInfo().username,uid:os.userInfo().uid,gid:os.userInfo().gid,home:os.homedir()}, owner: user ? String(user.id) : 'default', identity: { linuxUser: info ? info.user : null, osUserInfo: info } };
|
|
22
|
+
}
|
|
23
|
+
function signature() {
|
|
24
|
+
return JSON.stringify(Object.keys(registry.VENDOR_REGISTRY).map(function (id) {
|
|
25
|
+
var info = registry.getVendorInfo(id);
|
|
26
|
+
return [id, info.loginCommand, info.terminalLoginCommand, info.browserLoginCommand, info.oauthHosts, info.osUserIsolation, info.installRecipe];
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
function current(ws, receipt) {
|
|
30
|
+
if (!receipt) return false;
|
|
31
|
+
try { return Object.keys(receipt.registry).length === Object.keys(registry.VENDOR_REGISTRY).length && Object.keys(receipt.registry).every(function (id) { return receipt.registry[id] === registry.getVendorInfo(id) && receipt.resolvers[id] === registry.getVendorInfo(id).resolveExecutable; }) && receipt.commands === signature() && receipts.get(ws) === receipt && receipt.expires > Date.now() && JSON.stringify(actor(ws)) === receipt.actor; }
|
|
32
|
+
catch (error) { return false; }
|
|
33
|
+
}
|
|
34
|
+
function catalog(ws, requestId) {
|
|
35
|
+
var target = actor(ws);
|
|
36
|
+
var receipt = { registry: Object.assign({}, registry.VENDOR_REGISTRY), resolvers: {}, commands: signature(), id: crypto.randomUUID(), actor: JSON.stringify(target), expires: Date.now() + LIFETIME };
|
|
37
|
+
Object.keys(receipt.registry).forEach(function (id) { receipt.resolvers[id] = receipt.registry[id].resolveExecutable; });
|
|
38
|
+
receipts.set(ws, receipt);
|
|
39
|
+
var info = target.identity.osUserInfo;
|
|
40
|
+
var scope = info ? 'Mapped OS account ' + info.user + ' · HOME ' + info.home :
|
|
41
|
+
'Shared server OS account ' + os.userInfo().username + ' · HOME ' + os.homedir() + '. Provider credentials are shared by processes using this HOME; they are not isolated by Clay Studio account.';
|
|
42
|
+
var rows = Object.keys(registry.VENDOR_REGISTRY).map(function (id) {
|
|
43
|
+
var provider = registry.getVendorInfo(id);
|
|
44
|
+
var supported = !ctx.osUsers || provider.osUserIsolation === true;
|
|
45
|
+
var installed = null, reason = '';
|
|
46
|
+
if (!supported) reason = 'This provider does not support mapped OS accounts in Clay Studio.';
|
|
47
|
+
else {
|
|
48
|
+
try { installed = !!provider.resolveExecutable({ user: target.identity.linuxUser }); }
|
|
49
|
+
catch (error) { reason = 'Installation discovery failed. Check the server setup, then refresh status.'; }
|
|
50
|
+
}
|
|
51
|
+
var installReason = installRecipes.eligible(provider.installRecipe, target.runtime);
|
|
52
|
+
if (!ctx.osUsers && target.role !== 'admin') installReason = 'Only the server administrator can install into the shared runtime HOME.';
|
|
53
|
+
var login = false;
|
|
54
|
+
try { protocol.command(id, false); login = true; } catch (error) {}
|
|
55
|
+
var permitted = !ctx.requestAccess || ctx.requestAccess.hasPermission(ws, 'terminal');
|
|
56
|
+
var canInstall = supported && installed === false && permitted && !installReason;
|
|
57
|
+
return { id: id, name: provider.displayName, icon: provider.avatar, setupUrl: provider.setupUrl, installed: installed,
|
|
58
|
+
authentication: 'unknown', tested: false, browser: !!provider.browserLoginCommand,
|
|
59
|
+
canSignIn: supported && installed === true && login && permitted,
|
|
60
|
+
canInstall: canInstall, installReason: installReason,
|
|
61
|
+
reason: reason || (!permitted ? 'Terminal permission is required for provider installation and sign-in.' : ''),
|
|
62
|
+
setup: canInstall ? 'Review the official installer for the runtime account shown above. Installation does not sign in or run a task.' : (installed === null ? (reason || 'Installation status is unknown. Refresh status before reviewing an installation.') : installReason || 'Review the registered installer before running it.') + ' Ask the server operator to install ' + provider.binaryName + ' for the runtime OS account shown above, using the provider’s official setup instructions. Then refresh status.',
|
|
63
|
+
loginCommand: supported && permitted && login ? protocol.command(id, false) : null };
|
|
64
|
+
});
|
|
65
|
+
if (!current(ws, receipt)) throw new Error('Provider account context changed. Refresh status.');
|
|
66
|
+
return { type: 'vendor_login_catalog', requestId: requestId, revision: receipt.id, owner: target.owner, accountName:target.accountName, runtime:Object.assign({shared:!info},target.runtime), scope: scope, providers: rows };
|
|
67
|
+
}
|
|
68
|
+
function resolve(ws, msg) {
|
|
69
|
+
var receipt = receipts.get(ws), info = registry.getVendorInfo(msg.vendor);
|
|
70
|
+
if (!current(ws, receipt) || msg.personalRequestId !== receipt.id || msg.sessionId || msg.workerAuthRequestId ||
|
|
71
|
+
!info || ctx.osUsers && info.osUserIsolation !== true || ctx.requestAccess && !ctx.requestAccess.hasPermission(ws, 'terminal')) {
|
|
72
|
+
throw new Error('Personal provider context expired or is no longer permitted. Refresh status before signing in.');
|
|
73
|
+
}
|
|
74
|
+
var target = actor(ws);
|
|
75
|
+
if (!info.resolveExecutable({user:target.identity.linuxUser})) throw new Error('Install this provider for the runtime OS account, then refresh status.');
|
|
76
|
+
return { identity: target.identity, validate: function (peer) {
|
|
77
|
+
return peer === ws && current(ws, receipt) && (!ctx.requestAccess || ctx.requestAccess.hasPermission(ws, 'terminal'));
|
|
78
|
+
} };
|
|
79
|
+
}
|
|
80
|
+
function resolveInstall(ws, msg) {
|
|
81
|
+
var receipt = receipts.get(ws), provider = registry.getVendorInfo(msg.vendor);
|
|
82
|
+
if (!current(ws, receipt) || msg.personalRequestId !== receipt.id || msg.sessionId || msg.workerAuthRequestId || !provider || ctx.osUsers && provider.osUserIsolation !== true || ctx.requestAccess && !ctx.requestAccess.hasPermission(ws, 'terminal')) throw new Error('Installation context expired or permission changed. Refresh status.');
|
|
83
|
+
var target = actor(ws), recipe = provider.installRecipe;
|
|
84
|
+
var reason = installRecipes.eligible(recipe, target.runtime);
|
|
85
|
+
if (!ctx.osUsers && target.role !== 'admin') reason = 'Only the server administrator can install into the shared runtime HOME.';
|
|
86
|
+
if (reason) throw new Error(reason);
|
|
87
|
+
if (provider.resolveExecutable({user:target.identity.linuxUser})) throw new Error('This provider is already installed. Refresh status.');
|
|
88
|
+
return { recipe:recipe, runtime:target.runtime, identity:target.identity, validate:function () {
|
|
89
|
+
try { return current(ws, receipt) && provider === registry.getVendorInfo(msg.vendor) && recipe === provider.installRecipe && (!ctx.requestAccess || ctx.requestAccess.hasPermission(ws,'terminal')) && !installRecipes.eligible(recipe,actor(ws).runtime); } catch(error) { return false; }
|
|
90
|
+
} };
|
|
91
|
+
}
|
|
92
|
+
return { catalog: catalog, resolve: resolve, resolveInstall:resolveInstall, release: function (ws) { receipts.delete(ws); } };
|
|
93
|
+
}
|
|
94
|
+
module.exports = { attach: attach };
|
|
@@ -375,9 +375,9 @@ function attachProjectLogsService(ctx) {
|
|
|
375
375
|
};
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
function setContextState(projectSlug, status) {
|
|
378
|
+
function setContextState(projectSlug, status, captured) {
|
|
379
379
|
var project = projectFor(projectSlug);
|
|
380
|
-
var projectStatus = project && project.getStatus();
|
|
380
|
+
var projectStatus = captured || project && project.getStatus();
|
|
381
381
|
if (!projectStatus || !projectStatus.isWorktree || !projectStatus.changeSetId) return false;
|
|
382
382
|
return storeForStatus(projectStatus).setContextState(recordContext(projectStatus), status);
|
|
383
383
|
}
|
|
@@ -267,7 +267,7 @@ function attachPairTaskControl(ctx) {
|
|
|
267
267
|
};
|
|
268
268
|
}
|
|
269
269
|
function drain(caller, worker) {
|
|
270
|
-
if (ctx.turnControl.blockedReason(caller) || worker.isProcessing || worker._queryStarting || current(worker)) return false;
|
|
270
|
+
if (ctx.turnControl.blockedReason(caller) || worker._workerAuthRequired || worker.isProcessing || worker._queryStarting || current(worker)) return false;
|
|
271
271
|
var queue = queueFor(worker);
|
|
272
272
|
var next = null;
|
|
273
273
|
for (var i = 0; i < queue.length; i++) {
|
|
@@ -250,7 +250,7 @@ function createPendingMessageQueue(opts) {
|
|
|
250
250
|
}
|
|
251
251
|
function consumeOne(session, dispatch) {
|
|
252
252
|
var foreground = require('./sdk-foreground-admission');
|
|
253
|
-
if (loadError || !session || session._pendingMessageClaimId || isPaused(session) || session._pendingMessageDrainPaused || session.taskStopRequested || session._queryStarting || (session._awaitingTurnResult || session.isProcessing) && !foreground.available(session)) return null;
|
|
253
|
+
if (loadError || !session || session._workerAuthRequired || session._pendingMessageClaimId || isPaused(session) || session._pendingMessageDrainPaused || session.taskStopRequested || session._queryStarting || (session._awaitingTurnResult || session.isProcessing) && !foreground.available(session)) return null;
|
|
254
254
|
if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()) return null;
|
|
255
255
|
for (var i = 0; i < data.items.length; i++) {
|
|
256
256
|
var item = data.items[i];
|
|
@@ -325,7 +325,11 @@ function createPendingMessageQueue(opts) {
|
|
|
325
325
|
}
|
|
326
326
|
return null;
|
|
327
327
|
}
|
|
328
|
-
|
|
328
|
+
function retireClaim(session) {
|
|
329
|
+
if (!session._pendingMessageClaimId) return true;
|
|
330
|
+
return finishClaim(session, session._pendingMessageClaimId, false, "Force stopped during delivery. Inspect the transcript before sending this message again.");
|
|
331
|
+
}
|
|
332
|
+
return { retireClaim: retireClaim, admit: admit, upsertScheduled: upsertScheduled, releaseScheduled: releaseScheduled, cancelScheduled: cancelScheduled, edit: edit, cancel: cancel, reorder: reorder, pause: pause, resume: resume, consumeOne: consumeOne, hydrate: hydrate, inspect: inspect, hasActive: hasActive, isClaimed: isClaimed, isPaused: isPaused, list: visible, getRevision: function () { return data.revision; }, getLoadError: function () { return loadError; }, filePath: filePath };
|
|
329
333
|
}
|
|
330
334
|
|
|
331
335
|
module.exports = { createPendingMessageQueue: createPendingMessageQueue };
|
|
@@ -48,6 +48,7 @@ function attachProjectProductControl(ctx) {
|
|
|
48
48
|
if (action === "vendor_new_session") return ctx.vendor.newSession(session, value, current);
|
|
49
49
|
if (action === "runtime") return ctx.runtime.change(session, value, current);
|
|
50
50
|
if (action === "rename") return ctx.actions.rename(session, value);
|
|
51
|
+
if (action === "force_stop") return ctx.actions.forceStop(session, user);
|
|
51
52
|
if (action === "stop") return ctx.actions.stop(session, user);
|
|
52
53
|
if (session.taskStopRequested && action !== "delete") throw new Error("Target was stopped; explicit Driver resumption is required.");
|
|
53
54
|
if (action === "model") {
|
|
@@ -26,19 +26,62 @@ function attachSessionActions(ctx) {
|
|
|
26
26
|
}
|
|
27
27
|
return { title: title };
|
|
28
28
|
}
|
|
29
|
-
function stop(session, actor) {
|
|
29
|
+
function stop(session, actor, retiring) {
|
|
30
30
|
exact(session);
|
|
31
31
|
session._humanStopRevision = (session._humanStopRevision || 0) + 1;
|
|
32
32
|
if (ctx.autonomousRun && session.autonomousRun) ctx.autonomousRun.finish(session, "stopped", "Stopped by the user.");
|
|
33
33
|
if (typeof ctx.sdk.cancelResponseRecovery === "function") ctx.sdk.cancelResponseRecovery(session);
|
|
34
34
|
session._pendingMessageDrainPaused = true;
|
|
35
35
|
if (ctx.onPendingQueuePause) ctx.onPendingQueuePause(session, actor || null);
|
|
36
|
-
if (!session.isProcessing && !session._queryStarting && !session._responseRecoveryStopPending) return { status: "completed", queuePaused: true };
|
|
37
36
|
if (ctx.onHumanPairStop) ctx.onHumanPairStop(session);
|
|
37
|
+
if (!session.isProcessing && !session._queryStarting && !session._responseRecoveryStopPending && !session._awaitingTurnResult && !session.queryInstance) return { status: "completed", queuePaused: true };
|
|
38
38
|
session.taskStopRequested = true;
|
|
39
|
+
if (retiring) return { status: "queued", queuePaused: true };
|
|
39
40
|
if (session.abortController) session.abortController.abort();
|
|
41
|
+
else if (session.queryInstance && typeof session.queryInstance.abort === "function") session.queryInstance.abort();
|
|
40
42
|
return { status: "queued", queuePaused: true, detail: "Stop requested; runtime completion is still pending." };
|
|
41
43
|
}
|
|
44
|
+
async function forceStopOne(session, actor) {
|
|
45
|
+
exact(session);
|
|
46
|
+
var state = require('./session-force-stop-support').one(sm, session);
|
|
47
|
+
if (!state.supported) throw new Error(state.reason);
|
|
48
|
+
stop(session, actor, true);
|
|
49
|
+
var expectedRevision = (session._forceStopRevision || 0) + 1;
|
|
50
|
+
var expectedGeneration = Number(session._sdkQueryGeneration || 0) + 1, owner = session.ownerId, vendor = session.vendor;
|
|
51
|
+
var result = await ctx.sdk.forceStopSession(session);
|
|
52
|
+
if (sm.sessions.get(session.localId) !== session || session._forceStopRevision !== expectedRevision || session._sdkQueryGeneration !== expectedGeneration || session.ownerId !== owner || session.vendor !== vendor || session.queryInstance || session._queryStarting) return Object.assign({}, result, { status: 'failed', detail: 'Session runtime changed during termination.' });
|
|
53
|
+
if (ctx.usersModule && ctx.usersModule.isMultiUser() && (!actor || session.ownerId !== actor.id || !ctx.usersModule.canAccessSession(actor.id, session, ctx.getProjectAccess ? ctx.getProjectAccess() : {}))) return Object.assign({}, result, { status: 'failed', detail: 'Session access changed during termination.' });
|
|
54
|
+
if (ctx.onPendingQueueRetire && ctx.onPendingQueueRetire(session) === false) {
|
|
55
|
+
result.queueRecovery = "failed";
|
|
56
|
+
sm.sendAndRecord(session, { type: "error", text: "The connection was force stopped, but the pending delivery record could not be updated. Check queue storage before resuming." });
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
async function forceStop(session, actor) {
|
|
61
|
+
exact(session);
|
|
62
|
+
var support = require('./session-force-stop-support');
|
|
63
|
+
var targets = support.targets(sm, session), workers = targets.slice(1);
|
|
64
|
+
targets.forEach(function (target) {
|
|
65
|
+
exact(target);
|
|
66
|
+
if ((target.ownerId || null) !== (session.ownerId || null) || ctx.usersModule && ctx.usersModule.isMultiUser() &&
|
|
67
|
+
(!actor || target.ownerId !== actor.id || !ctx.usersModule.canAccessSession(actor.id, target, ctx.getProjectAccess ? ctx.getProjectAccess() : {}))) throw new Error("One of the session's Workers is no longer accessible.");
|
|
68
|
+
});
|
|
69
|
+
targets.forEach(function (target) {
|
|
70
|
+
var state = support.one(sm, target);
|
|
71
|
+
if (!state.supported) throw new Error(state.reason + (target === session ? '' : ' (owned Worker)'));
|
|
72
|
+
});
|
|
73
|
+
// Start all exact-query effects synchronously after the complete preflight.
|
|
74
|
+
var results = await Promise.all(targets.map(async function (target) {
|
|
75
|
+
try { return Object.assign({ sessionId: target.localId }, await forceStopOne(target, actor)); }
|
|
76
|
+
catch (error) { return { sessionId: target.localId, status: "failed", error: error.message || String(error) }; }
|
|
77
|
+
}));
|
|
78
|
+
var result = Object.assign({}, results[0], { workerCount: workers.length, sessions: results });
|
|
79
|
+
if (results.some(function (item) { return item.status === "failed" || item.queueRecovery === "failed"; })) {
|
|
80
|
+
result.status = "failed"; result.outcome = "partial";
|
|
81
|
+
if (sm.sendAndRecord) sm.sendAndRecord(session, { type: "error", text: "Some session or Worker force-stop actions failed. Inspect their state before continuing." });
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
42
85
|
async function effort(session, value, revalidate) {
|
|
43
86
|
exact(session);
|
|
44
87
|
var vendor = session.vendor || sm.defaultVendor || "claude";
|
|
@@ -61,6 +104,6 @@ function attachSessionActions(ctx) {
|
|
|
61
104
|
sm.sendToSession(session, Object.assign({ type: "config_state", sessionId: session.localId, vendor: session.vendor || sm.defaultVendor || "claude", mode: state.requestedPermissionMode, permissionRequestId: requestId || null, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] }, state));
|
|
62
105
|
return state;
|
|
63
106
|
}
|
|
64
|
-
return { rename: rename, stop: stop, effort: effort, permission: permission };
|
|
107
|
+
return { rename: rename, stop: stop, forceStop: forceStop, effort: effort, permission: permission };
|
|
65
108
|
}
|
|
66
109
|
module.exports = { attachSessionActions: attachSessionActions };
|
|
@@ -227,6 +227,8 @@ function attachSessionPair(ctx) {
|
|
|
227
227
|
broadcastDelegation(resolved.group, caller, partner, true);
|
|
228
228
|
var sdk = ctx.getSdk();
|
|
229
229
|
if (!sdk) throw new Error("SDK bridge is not ready");
|
|
230
|
+
partner._workerAuthRequired = false;
|
|
231
|
+
if (partner._forceStopRevision && !partner.isProcessing && !partner._queryStarting && !partner.queryInstance) partner.taskStopRequested = false;
|
|
230
232
|
if (!partner.isProcessing) {
|
|
231
233
|
partner.isProcessing = true;
|
|
232
234
|
ctx.onProcessingChanged();
|
package/lib/project-sessions.js
CHANGED
|
@@ -1112,7 +1112,7 @@ function attachSessions(ctx) {
|
|
|
1112
1112
|
return true;
|
|
1113
1113
|
}
|
|
1114
1114
|
|
|
1115
|
-
if (msg.type === "stop") {
|
|
1115
|
+
if (msg.type === "stop" || msg.type === "force_stop") {
|
|
1116
1116
|
var session = getSessionForWs(ws);
|
|
1117
1117
|
if (msg.sessionId != null || msg.sessionOriginId != null) {
|
|
1118
1118
|
session = sm.sessions.get(msg.sessionId);
|
|
@@ -1120,7 +1120,15 @@ function attachSessions(ctx) {
|
|
|
1120
1120
|
!usersModule.isMultiUser() && session.ownerId ||
|
|
1121
1121
|
usersModule.isMultiUser() && (!ws._clayUser || !usersModule.canAccessSession(ws._clayUser.id, session, getProjectAccess()))) return true;
|
|
1122
1122
|
}
|
|
1123
|
-
|
|
1123
|
+
if (!session) return true;
|
|
1124
|
+
if (msg.workerGeneration != null || msg.parentSessionOriginId != null) {
|
|
1125
|
+
var lineage = session.sessionProvenance;
|
|
1126
|
+
if (!lineage || lineage.kind !== "worker" || lineage.generation !== msg.workerGeneration || lineage.parentSessionOriginId !== msg.parentSessionOriginId) return true;
|
|
1127
|
+
}
|
|
1128
|
+
if (msg.type === "force_stop") sessionActions.forceStop(session, ws && ws._clayUser).catch(function (error) {
|
|
1129
|
+
sm.sendToSession(session, { type: 'error', text: error.message || String(error) });
|
|
1130
|
+
});
|
|
1131
|
+
else stopSession(session, ws && ws._clayUser);
|
|
1124
1132
|
return true;
|
|
1125
1133
|
}
|
|
1126
1134
|
|
|
@@ -1917,7 +1925,7 @@ function attachSessions(ctx) {
|
|
|
1917
1925
|
return true;
|
|
1918
1926
|
}
|
|
1919
1927
|
if (typeof opts.onSetProjectIcon === "function") {
|
|
1920
|
-
var iconResult = opts.onSetProjectIcon(msg.slug, msg.icon || null);
|
|
1928
|
+
var iconResult = opts.onSetProjectIcon(msg.slug, msg.icon || null, ws._clayUser ? ws._clayUser.id : null);
|
|
1921
1929
|
sendTo(ws, { type: "set_project_icon_result", ok: iconResult.ok, slug: msg.slug, error: iconResult.error, requestId: iconRequestId });
|
|
1922
1930
|
} else {
|
|
1923
1931
|
sendTo(ws, { type: "set_project_icon_result", ok: false, error: "Not supported", requestId: iconRequestId });
|
|
@@ -141,6 +141,7 @@ function attachUserMessage(ctx) {
|
|
|
141
141
|
queueResult = pendingMessageQueue.resume(queueSession, queueActor, msg.revision, msg.sessionId);
|
|
142
142
|
if (queueResult.ok && !queueResult.paused) {
|
|
143
143
|
queueSession._pendingMessageDrainPaused = false;
|
|
144
|
+
if (queueSession._forceStopRevision && !queueSession.isProcessing && !queueSession._queryStarting && !queueSession.queryInstance) queueSession.taskStopRequested = false;
|
|
144
145
|
setImmediate(function () { consumePendingMessage(queueSession); });
|
|
145
146
|
}
|
|
146
147
|
}
|
|
@@ -505,7 +506,10 @@ function attachUserMessage(ctx) {
|
|
|
505
506
|
}
|
|
506
507
|
return true;
|
|
507
508
|
}
|
|
508
|
-
if (!pendingInternal)
|
|
509
|
+
if (!pendingInternal) {
|
|
510
|
+
session._pendingMessageDrainPaused = false;
|
|
511
|
+
if (session._forceStopRevision && !session.isProcessing && !session._queryStarting && !session.queryInstance) session.taskStopRequested = false;
|
|
512
|
+
}
|
|
509
513
|
if (msg.autonomousRunToken) {
|
|
510
514
|
if (!ctx.autonomousRun || !ctx.autonomousRun.consume(session, msg)) {
|
|
511
515
|
sendTo(ws, { type: "error", text: "This Until complete submission is stale or no longer armed." });
|
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Owns the "the vendor CLI is not logged in" recovery path end to end so the
|
|
4
|
-
// client never has to guess. One login terminal per vendor per requester is the
|
|
5
|
-
// single source of truth: repeated auth_required events (other sessions, split
|
|
6
|
-
// panes, a burst of 401 retries) re-use that record instead of spawning more
|
|
7
|
-
// terminals.
|
|
8
|
-
//
|
|
9
|
-
// Lifecycle:
|
|
10
|
-
// vendor_login_start -> create (or re-use) a PTY running the vendor login
|
|
11
|
-
// command, reply with vendor_login_ready
|
|
12
|
-
// PTY output/exit -> detect a successful login, restart the vendor's
|
|
13
|
-
// YOKE adapter so it re-reads the credential file,
|
|
14
|
-
// send auth_refreshed, close the terminal and browser
|
|
15
|
-
// vendor_login_cancel -> user dismissed the flow; kill the terminal
|
|
16
|
-
//
|
|
17
|
-
// The adapter restart is the actual bug fix: a Codex app-server is a long-lived
|
|
18
|
-
// child process that reads ~/.codex/auth.json once at spawn, so without a
|
|
19
|
-
// restart every query after a successful login keeps hitting 401 and re-arms
|
|
20
|
-
// auth_required forever.
|
|
21
|
-
|
|
1
|
+
// One requester-bound provider sign-in flow; auth refresh never replays a task.
|
|
22
2
|
var vendorRegistry = require("./yoke/vendor-registry");
|
|
23
3
|
var yoke = require("./yoke");
|
|
24
4
|
|
|
@@ -41,6 +21,8 @@ var looksLikeLoginSuccess = loginProtocol.successful;
|
|
|
41
21
|
* refreshVendorAuthEverywhere
|
|
42
22
|
*/
|
|
43
23
|
function attachVendorLogin(ctx) {
|
|
24
|
+
var personal = require('./personal-provider-settings').attach(ctx);
|
|
25
|
+
var installation = require('./personal-provider-install').attach(ctx,personal);
|
|
44
26
|
var slug = ctx.slug;
|
|
45
27
|
var osUsers = ctx.osUsers;
|
|
46
28
|
|
|
@@ -61,12 +43,14 @@ function attachVendorLogin(ctx) {
|
|
|
61
43
|
// Requester + vendor -> flow. Credentials and sign-in views stay user-bound.
|
|
62
44
|
var _flows = Object.create(null);
|
|
63
45
|
function flowKey(ws, vendor) {
|
|
64
|
-
|
|
46
|
+
var info = osUsers && getOsUserInfoForWs ? getOsUserInfoForWs(ws) : null;
|
|
47
|
+
return JSON.stringify([ws && ws._clayUser ? String(ws._clayUser.id) : null, vendor, info]);
|
|
65
48
|
}
|
|
66
49
|
var browserLogin = require("./vendor-login-browser-flow").attachBrowserLogin({
|
|
67
50
|
osUsers: osUsers, sendTo: sendTo, hasTerminalPermission: hasTerminalPermission,
|
|
68
51
|
createLoginBrowser: ctx.createLoginBrowser,
|
|
69
52
|
flowKey: flowKey,
|
|
53
|
+
validate: function (ws, flow) { return !flow.validate || flow.validate(flow.requester) && flowKey(ws, flow.vendor) === flow.key; },
|
|
70
54
|
cancel: function (vendor) { closeFlowTerminal(discardFlow(vendor)); broadcastState(); },
|
|
71
55
|
});
|
|
72
56
|
|
|
@@ -82,13 +66,15 @@ function attachVendorLogin(ctx) {
|
|
|
82
66
|
var vendors = Object.keys(_flows);
|
|
83
67
|
for (var i = 0; i < vendors.length; i++) {
|
|
84
68
|
var flow = _flows[vendors[i]];
|
|
85
|
-
if (ws && flowKey(ws, flow.vendor) !== vendors[i]) continue;
|
|
69
|
+
if (ws && (flowKey(ws, flow.vendor) !== vendors[i] || flow.validate && !flow.validate(flow.requester))) continue;
|
|
86
70
|
result.push({
|
|
87
71
|
vendor: flow.vendor,
|
|
88
72
|
terminalId: flow.terminalId,
|
|
89
73
|
startedAt: flow.startedAt,
|
|
90
74
|
completed: !!flow.completed,
|
|
91
75
|
browserId: flow.browser ? flow.browser.id : null,
|
|
76
|
+
workerAuthRequestId: flow.workerAuthRequestId,
|
|
77
|
+
personalRequestId: flow.personalRequestId,
|
|
92
78
|
});
|
|
93
79
|
}
|
|
94
80
|
return result;
|
|
@@ -150,6 +136,8 @@ function attachVendorLogin(ctx) {
|
|
|
150
136
|
delete _flows[vendor];
|
|
151
137
|
if (flow.browser) flow.browser.close();
|
|
152
138
|
if (flow.expiryTimer) clearTimeout(flow.expiryTimer);
|
|
139
|
+
if (flow.validationTimer) clearInterval(flow.validationTimer);
|
|
140
|
+
if (flow.onClose && flow.requester.removeListener) flow.requester.removeListener('close', flow.onClose);
|
|
153
141
|
if (flow.closeTimer) {
|
|
154
142
|
clearTimeout(flow.closeTimer);
|
|
155
143
|
flow.closeTimer = null;
|
|
@@ -201,6 +189,7 @@ function attachVendorLogin(ctx) {
|
|
|
201
189
|
function finishFlow(vendor, succeeded) {
|
|
202
190
|
var flow = _flows[vendor];
|
|
203
191
|
if (!flow || flow.finishing) return;
|
|
192
|
+
if (flow.validate && !flow.validate(flow.requester)) { closeFlowTerminal(discardFlow(vendor)); broadcastState(); return; }
|
|
204
193
|
flow.finishing = true;
|
|
205
194
|
flow.completed = !!succeeded;
|
|
206
195
|
|
|
@@ -210,26 +199,36 @@ function attachVendorLogin(ctx) {
|
|
|
210
199
|
if (succeeded) yoke.invalidateAuthCache();
|
|
211
200
|
var refresh = Promise.resolve(false);
|
|
212
201
|
if (succeeded) {
|
|
213
|
-
refresh =
|
|
214
|
-
|
|
215
|
-
|
|
202
|
+
refresh = Promise.resolve().then(function () {
|
|
203
|
+
if (_flows[vendor] !== flow || flow.validate && !flow.validate(flow.requester)) return false;
|
|
204
|
+
return typeof refreshVendorAuthEverywhere === "function"
|
|
205
|
+
? refreshVendorAuthEverywhere(flow.vendor, flow.linuxUser || null)
|
|
206
|
+
: refreshVendorAuth(flow.vendor, flow.linuxUser || null);
|
|
207
|
+
});
|
|
216
208
|
}
|
|
217
209
|
refresh.then(function (restarted) {
|
|
218
210
|
if (_flows[vendor] !== flow) return;
|
|
211
|
+
if (flow.validate && !flow.validate(flow.requester)) { closeFlowTerminal(discardFlow(vendor)); broadcastState(); return; }
|
|
219
212
|
discardFlow(vendor);
|
|
220
213
|
closeFlowTerminal(flow);
|
|
221
214
|
if (succeeded) {
|
|
222
|
-
var message = { type: "auth_refreshed", slug: slug, vendor: flow.vendor, adapterRestarted: !!restarted };
|
|
215
|
+
var message = { type: "auth_refreshed", slug: slug, vendor: flow.vendor, adapterRestarted: !!restarted, workerAuthRequestId: flow.workerAuthRequestId, personalRequestId: flow.personalRequestId };
|
|
223
216
|
if (ctx.clients) ctx.clients.forEach(function (ws) { if (flowKey(ws, flow.vendor) === vendor && hasTerminalPermission(ws)) sendTo(ws, message); });
|
|
224
217
|
else send(message);
|
|
225
218
|
}
|
|
226
219
|
broadcastState();
|
|
220
|
+
}).catch(function (error) {
|
|
221
|
+
if (_flows[vendor] !== flow) return;
|
|
222
|
+
closeFlowTerminal(discardFlow(vendor));
|
|
223
|
+
sendTo(flow.requester, { type: 'vendor_login_error', vendor: flow.vendor, personalRequestId: flow.personalRequestId, error: 'Sign-in ended, but refreshing the provider failed. Check its status before explicitly retrying the task.' });
|
|
224
|
+
broadcastState();
|
|
227
225
|
});
|
|
228
226
|
}
|
|
229
227
|
|
|
230
228
|
function handleFlowOutput(vendor, chunk) {
|
|
231
229
|
var flow = _flows[vendor];
|
|
232
230
|
if (!flow || flow.finishing || flow.succeeded) return;
|
|
231
|
+
if (flow.validate && !flow.validate(flow.requester)) { closeFlowTerminal(discardFlow(vendor)); broadcastState(); return; }
|
|
233
232
|
flow.outputTail = (flow.outputTail + stripAnsi(chunk)).slice(-OUTPUT_TAIL_MAX);
|
|
234
233
|
if (flow.browser) flow.browser.output(stripAnsi(chunk));
|
|
235
234
|
if (!looksLikeLoginSuccess(flow.outputTail)) return;
|
|
@@ -252,9 +251,13 @@ function attachVendorLogin(ctx) {
|
|
|
252
251
|
finishFlow(vendor, !!flow.succeeded);
|
|
253
252
|
}
|
|
254
253
|
|
|
255
|
-
function startFlow(ws, vendor, msg) {
|
|
254
|
+
function startFlow(ws, vendor, msg, authorization) {
|
|
256
255
|
var key = flowKey(ws, vendor);
|
|
257
|
-
var identity = resolveLoginIdentity(ws, msg);
|
|
256
|
+
var identity = msg.personalRequestId && authorization ? authorization.identity : resolveLoginIdentity(ws, msg);
|
|
257
|
+
if (osUsers && (!identity.linuxUser || !identity.osUserInfo) || authorization &&
|
|
258
|
+
(!authorization.validate(ws) || JSON.stringify(identity) !== JSON.stringify(authorization.identity))) {
|
|
259
|
+
throw new Error('The provider credential identity is unavailable or changed. Retry sign-in from its current session.');
|
|
260
|
+
}
|
|
258
261
|
var command = loginCommandFor(vendor);
|
|
259
262
|
var browserMode = msg.mode === "browser" && !!(vendorRegistry.getVendorInfo(vendor) || {}).browserLoginCommand;
|
|
260
263
|
if (browserMode) command = loginProtocol.command(vendor, true);
|
|
@@ -271,6 +274,10 @@ function attachVendorLogin(ctx) {
|
|
|
271
274
|
closing: false,
|
|
272
275
|
completed: false,
|
|
273
276
|
closeTimer: null,
|
|
277
|
+
validate: authorization && authorization.validate,
|
|
278
|
+
requester: ws,
|
|
279
|
+
workerAuthRequestId: msg.workerAuthRequestId || null,
|
|
280
|
+
personalRequestId: msg.personalRequestId || null,
|
|
274
281
|
};
|
|
275
282
|
// Registered before create() so the PTY's first output chunk (hooks fire
|
|
276
283
|
// synchronously from spawn) already finds the record.
|
|
@@ -298,12 +305,37 @@ function attachVendorLogin(ctx) {
|
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
flow.terminalId = terminal.id;
|
|
308
|
+
if (flow.validate) {
|
|
309
|
+
flow.onClose = function () { closeFlowTerminal(discardFlow(key)); broadcastState(); };
|
|
310
|
+
if (ws.once) ws.once('close', flow.onClose);
|
|
311
|
+
flow.validationTimer = setInterval(function () {
|
|
312
|
+
if (_flows[key] === flow && !flow.validate(ws)) flow.onClose();
|
|
313
|
+
}, 1000);
|
|
314
|
+
if (flow.validationTimer.unref) flow.validationTimer.unref();
|
|
315
|
+
}
|
|
301
316
|
if (!flow.browser) tm.attach(terminal.id, ws);
|
|
302
317
|
send({ type: "term_list", terminals: tm.list(true) });
|
|
303
318
|
return flow;
|
|
304
319
|
}
|
|
305
320
|
|
|
306
321
|
function handleVendorLoginMessage(ws, msg) {
|
|
322
|
+
if (installation.handle(ws,msg)) return true;
|
|
323
|
+
if (msg.type === 'vendor_login_catalog_request' || msg.type === 'vendor_login_personal_release') {
|
|
324
|
+
installation.release(ws); personal.release(ws);
|
|
325
|
+
Object.keys(_flows).forEach(function (key) { var flow = _flows[key]; if (flow.requester === ws && flow.personalRequestId) closeFlowTerminal(discardFlow(key)); });
|
|
326
|
+
if (msg.type === 'vendor_login_catalog_request') {
|
|
327
|
+
try { sendTo(ws, personal.catalog(ws, msg.requestId)); }
|
|
328
|
+
catch (error) { sendTo(ws, { type: 'vendor_login_catalog', requestId: msg.requestId, error: error.message }); }
|
|
329
|
+
}
|
|
330
|
+
broadcastState(); return true;
|
|
331
|
+
}
|
|
332
|
+
var scoped = _flows[flowKey(ws, String(msg.vendor || 'claude'))];
|
|
333
|
+
if (typeof msg.type === 'string' && msg.type.indexOf('vendor_login_') === 0 && scoped && scoped.validate && !scoped.validate(scoped.requester)) {
|
|
334
|
+
closeFlowTerminal(discardFlow(scoped.key));
|
|
335
|
+
sendTo(ws, { type: 'vendor_login_error', vendor: scoped.vendor, personalRequestId: scoped.personalRequestId, error: 'Sign-in context changed. Reopen provider settings or explicitly retry the task.' });
|
|
336
|
+
broadcastState();
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
307
339
|
if (browserLogin.handle(ws, msg, _flows)) return true;
|
|
308
340
|
if (msg.type === "vendor_login_state_request") {
|
|
309
341
|
sendStateTo(ws);
|
|
@@ -312,6 +344,8 @@ function attachVendorLogin(ctx) {
|
|
|
312
344
|
|
|
313
345
|
if (msg.type === "vendor_login_cancel") {
|
|
314
346
|
var cancelVendor = String(msg.vendor || "");
|
|
347
|
+
if (scoped && (msg.personalRequestId || null) !== (scoped.personalRequestId || null)) return true;
|
|
348
|
+
if (scoped && msg.workerAuthRequestId && msg.workerAuthRequestId !== scoped.workerAuthRequestId) return true;
|
|
315
349
|
var cancelled = discardFlow(flowKey(ws, cancelVendor));
|
|
316
350
|
if (cancelled) closeFlowTerminal(cancelled);
|
|
317
351
|
broadcastState();
|
|
@@ -321,11 +355,24 @@ function attachVendorLogin(ctx) {
|
|
|
321
355
|
if (msg.type === "vendor_login_start") {
|
|
322
356
|
var vendor = String(msg.vendor || "claude");
|
|
323
357
|
if (!vendorRegistry.getVendorInfo(vendor)) {
|
|
324
|
-
sendTo(ws, { type: "vendor_login_error", vendor: vendor, error: "Unknown vendor: " + vendor });
|
|
358
|
+
sendTo(ws, { type: "vendor_login_error", vendor: vendor, personalRequestId: msg.personalRequestId || null, error: "Unknown vendor: " + vendor });
|
|
325
359
|
return true;
|
|
326
360
|
}
|
|
327
361
|
if (!hasTerminalPermission(ws)) {
|
|
328
|
-
sendTo(ws, { type: "vendor_login_error", vendor: vendor, error: "Terminal access is not permitted" });
|
|
362
|
+
sendTo(ws, { type: "vendor_login_error", vendor: vendor, personalRequestId: msg.personalRequestId || null, error: "Terminal access is not permitted" });
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
var authorization = null;
|
|
366
|
+
try {
|
|
367
|
+
var targetSession = sm && sm.sessions && sm.sessions.get(msg.sessionId);
|
|
368
|
+
if (msg.personalRequestId) authorization = personal.resolve(ws, msg);
|
|
369
|
+
else if (msg.workerAuthRequestId || require('./session-provenance').isWorker(targetSession)) {
|
|
370
|
+
if (!ctx.workerAuth) throw new Error('Worker sign-in is unavailable.');
|
|
371
|
+
authorization = ctx.workerAuth.resolve(ws, msg);
|
|
372
|
+
}
|
|
373
|
+
loginCommandFor(vendor);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
sendTo(ws, { type: 'vendor_login_error', vendor: vendor, personalRequestId: msg.personalRequestId || null, error: error.message || 'Provider sign-in is unavailable. Sign in manually on the server.' });
|
|
329
376
|
return true;
|
|
330
377
|
}
|
|
331
378
|
|
|
@@ -337,6 +384,9 @@ function attachVendorLogin(ctx) {
|
|
|
337
384
|
existing = null;
|
|
338
385
|
}
|
|
339
386
|
|
|
387
|
+
if (existing && (existing.personalRequestId || null) !== (msg.personalRequestId || null)) {
|
|
388
|
+
sendTo(ws, {type:'vendor_login_error', vendor:vendor, personalRequestId:msg.personalRequestId || null, error:'A sign-in is already running in another context. Finish or cancel it before starting another.'}); return true;
|
|
389
|
+
}
|
|
340
390
|
if (existing) {
|
|
341
391
|
// An auto-triggered request (a fresh auth_required from another
|
|
342
392
|
// session or split pane) must not pop a second prompt; it only learns
|
|
@@ -350,17 +400,26 @@ function attachVendorLogin(ctx) {
|
|
|
350
400
|
terminalId: existing.terminalId,
|
|
351
401
|
reused: true,
|
|
352
402
|
browserId: existing.browser ? existing.browser.id : null,
|
|
403
|
+
sessionId: msg.sessionId || null,
|
|
404
|
+
workerAuthRequestId: msg.workerAuthRequestId || null,
|
|
405
|
+
personalRequestId: msg.personalRequestId || null,
|
|
353
406
|
});
|
|
354
407
|
}
|
|
355
408
|
sendStateTo(ws);
|
|
356
409
|
return true;
|
|
357
410
|
}
|
|
358
411
|
|
|
359
|
-
var started
|
|
412
|
+
var started;
|
|
413
|
+
try { started = startFlow(ws, vendor, msg, authorization); }
|
|
414
|
+
catch (error) {
|
|
415
|
+
sendTo(ws, { type: 'vendor_login_error', vendor: vendor, personalRequestId: msg.personalRequestId || null, error: error.message || 'Provider sign-in is unavailable.' });
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
360
418
|
if (!started) {
|
|
361
419
|
sendTo(ws, {
|
|
362
420
|
type: "vendor_login_error",
|
|
363
421
|
vendor: vendor,
|
|
422
|
+
personalRequestId: msg.personalRequestId || null,
|
|
364
423
|
error: "Cannot create terminal (node-pty not available or limit reached)",
|
|
365
424
|
});
|
|
366
425
|
return true;
|
|
@@ -374,6 +433,9 @@ function attachVendorLogin(ctx) {
|
|
|
374
433
|
terminalId: started.terminalId,
|
|
375
434
|
reused: false,
|
|
376
435
|
browserId: started.browser ? started.browser.id : null,
|
|
436
|
+
sessionId: msg.sessionId || null,
|
|
437
|
+
workerAuthRequestId: msg.workerAuthRequestId || null,
|
|
438
|
+
personalRequestId: msg.personalRequestId || null,
|
|
377
439
|
});
|
|
378
440
|
broadcastState();
|
|
379
441
|
return true;
|
|
@@ -412,6 +474,7 @@ function attachVendorLogin(ctx) {
|
|
|
412
474
|
isLoginTerminal: isLoginTerminal,
|
|
413
475
|
refreshVendorAuth: refreshVendorAuth,
|
|
414
476
|
destroy: function () {
|
|
477
|
+
installation.destroy();
|
|
415
478
|
Object.keys(_flows).forEach(function (vendor) { closeFlowTerminal(discardFlow(vendor)); });
|
|
416
479
|
},
|
|
417
480
|
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Live owner-only authentication receipts. Never reconstructed from history.
|
|
2
|
+
var crypto = require('crypto');
|
|
3
|
+
var provenance = require('./session-provenance');
|
|
4
|
+
var LIFETIME = 5 * 60 * 1000;
|
|
5
|
+
function attach(ctx) {
|
|
6
|
+
var receipts = new WeakMap();
|
|
7
|
+
function identity(ws) {
|
|
8
|
+
return ctx.osUsers ? ctx.getOsUserInfoForWs(ws) : null;
|
|
9
|
+
}
|
|
10
|
+
function visible(ws, pair) {
|
|
11
|
+
return ws && ws.readyState === 1 && ctx.clients.has(ws) && !ws._clayPaneActivation &&
|
|
12
|
+
(ws._clayActiveSession === pair.driver.localId || ws._clayActiveSession === pair.worker.localId);
|
|
13
|
+
}
|
|
14
|
+
function valid(ws, receipt) {
|
|
15
|
+
if (!receipt || receipt.expires <= Date.now()) return false;
|
|
16
|
+
var pair = ctx.currentPair(ws, receipt.target);
|
|
17
|
+
return !!(pair && visible(ws, pair) && pair.driver === receipt.driver && pair.worker === receipt.worker &&
|
|
18
|
+
JSON.stringify([pair.group.id, pair.group.members, pair.group.pair]) === receipt.group &&
|
|
19
|
+
!pair.worker.hidden && !pair.driver.hidden && pair.worker.vendor === receipt.vendor && pair.linuxUser === receipt.linuxUser && !pair.worker.taskStopRequested &&
|
|
20
|
+
!pair.driver.taskStopRequested && !(pair.driver._pairTurnControl && pair.driver._pairTurnControl.humanStopped) &&
|
|
21
|
+
(pair.driver._forceStopRevision || 0) === receipt.driverStop &&
|
|
22
|
+
(pair.worker._forceStopRevision || 0) === receipt.stop &&
|
|
23
|
+
(pair.worker._queryGeneration || 0) === receipt.query &&
|
|
24
|
+
ctx.requestAccess.hasPermission(ws, 'terminal') &&
|
|
25
|
+
JSON.stringify(identity(ws)) === receipt.identity);
|
|
26
|
+
}
|
|
27
|
+
function emit(worker, message) {
|
|
28
|
+
if (!provenance.isWorker(worker)) return false;
|
|
29
|
+
if (ctx.sm.sessions.get(worker.localId) !== worker) return true;
|
|
30
|
+
worker._workerAuthRequired = true;
|
|
31
|
+
if (worker._workerAuthFailureQuery !== worker._queryGeneration || !worker._workerAuthFailureRecorded) {
|
|
32
|
+
worker._workerAuthFailureQuery = worker._queryGeneration;
|
|
33
|
+
worker._workerAuthFailureRecorded = true;
|
|
34
|
+
ctx.sm.sendAndRecord(worker, { type: 'error', text: 'The Worker needs provider sign-in. Complete sign-in, then explicitly retry the delegated task. No task was replayed.' });
|
|
35
|
+
}
|
|
36
|
+
var target = { parentSessionOriginId: worker.sessionProvenance.parentSessionOriginId,
|
|
37
|
+
workerSessionOriginId: worker.sessionOriginId, generation: worker.sessionProvenance.generation };
|
|
38
|
+
ctx.clients.forEach(function (ws) {
|
|
39
|
+
var pair = ctx.currentPair(ws, target);
|
|
40
|
+
if (!pair || pair.worker !== worker || !visible(ws, pair) || worker.hidden || worker.taskStopRequested) return;
|
|
41
|
+
var entries = receipts.get(ws) || new Map();
|
|
42
|
+
entries.forEach(function (entry, id) { if (!valid(ws, entry)) entries.delete(id); });
|
|
43
|
+
var receipt;
|
|
44
|
+
entries.forEach(function (entry) { if (entry.worker === worker) receipt = entry; });
|
|
45
|
+
if (!receipt) {
|
|
46
|
+
receipt = { id: crypto.randomUUID(), target: target, driver: pair.driver, worker: worker,
|
|
47
|
+
group: JSON.stringify([pair.group.id, pair.group.members, pair.group.pair]), vendor: worker.vendor,
|
|
48
|
+
identity: JSON.stringify(identity(ws)), linuxUser: pair.linuxUser, query: worker._queryGeneration || 0,
|
|
49
|
+
stop: worker._forceStopRevision || 0, driverStop: pair.driver._forceStopRevision || 0, expires: Date.now() + LIFETIME };
|
|
50
|
+
if (entries.size >= 16) entries.delete(entries.keys().next().value);
|
|
51
|
+
entries.set(receipt.id, receipt); receipts.set(ws, entries);
|
|
52
|
+
}
|
|
53
|
+
if (!valid(ws, receipt)) return;
|
|
54
|
+
ctx.sendTo(ws, { type: 'worker_auth_required', slug: ctx.slug, vendor: worker.vendor,
|
|
55
|
+
sessionId: worker.localId, workerAuthRequestId: receipt.id, text: message.text,
|
|
56
|
+
canAutoLogin: message.canAutoLogin, loginCommand: message.loginCommand });
|
|
57
|
+
});
|
|
58
|
+
// Worker events use this owner flow even when no eligible connection exists.
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
function resolve(ws, msg) {
|
|
62
|
+
var entries = receipts.get(ws), receipt = entries && entries.get(msg.workerAuthRequestId);
|
|
63
|
+
if (!valid(ws, receipt) || msg.vendor !== receipt.vendor || msg.sessionId !== receipt.worker.localId) {
|
|
64
|
+
throw new Error('This Worker sign-in request expired or its context changed. Retry the Worker explicitly to request sign-in again.');
|
|
65
|
+
}
|
|
66
|
+
return { identity: { linuxUser: receipt.linuxUser || null, osUserInfo: JSON.parse(receipt.identity) },
|
|
67
|
+
validate: function (peer) { return peer === ws && valid(ws, receipt); } };
|
|
68
|
+
}
|
|
69
|
+
return { emit: emit, resolve: resolve };
|
|
70
|
+
}
|
|
71
|
+
module.exports = { attach: attach };
|