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.
Files changed (115) hide show
  1. package/lib/clay-product-control-input.js +2 -2
  2. package/lib/clay-product-control-tools.js +1 -1
  3. package/lib/clay-product-control.js +4 -3
  4. package/lib/daemon-project-access.js +10 -2
  5. package/lib/daemon-projects.js +16 -4
  6. package/lib/daemon-worktree-controls.js +171 -0
  7. package/lib/daemon.js +14 -63
  8. package/lib/personal-provider-install.js +85 -0
  9. package/lib/personal-provider-settings.js +94 -0
  10. package/lib/project-logs-service.js +2 -2
  11. package/lib/project-pair-task-control.js +1 -1
  12. package/lib/project-pending-message-queue.js +6 -2
  13. package/lib/project-product-control.js +1 -0
  14. package/lib/project-session-actions.js +46 -3
  15. package/lib/project-session-pair.js +2 -0
  16. package/lib/project-sessions.js +11 -3
  17. package/lib/project-user-message.js +5 -1
  18. package/lib/project-vendor-login.js +95 -32
  19. package/lib/project-worker-auth.js +71 -0
  20. package/lib/project-worker-conversation.js +11 -2
  21. package/lib/project.js +9 -0
  22. package/lib/public/app.js +22 -0
  23. package/lib/public/css/icon-strip.css +5 -0
  24. package/lib/public/css/overlays.css +18 -0
  25. package/lib/public/css/settings-shell.css +13 -0
  26. package/lib/public/css/settings-ui.css +9 -0
  27. package/lib/public/css/split-view-layout.css +16 -11
  28. package/lib/public/modules/agent-behavior-nav.js +1 -0
  29. package/lib/public/modules/agent-message-bubbles.js +33 -4
  30. package/lib/public/modules/app-messages.js +7 -1
  31. package/lib/public/modules/app-projects.js +3 -4
  32. package/lib/public/modules/clay-control-copy.js +1 -0
  33. package/lib/public/modules/clay-tool-presentation.js +1 -1
  34. package/lib/public/modules/generation-stop.js +4 -4
  35. package/lib/public/modules/pane-activation.js +2 -2
  36. package/lib/public/modules/pane-bridge.js +4 -2
  37. package/lib/public/modules/personal-provider-context.js +11 -0
  38. package/lib/public/modules/project-settings.js +1 -1
  39. package/lib/public/modules/settings-section-nav.js +58 -0
  40. package/lib/public/modules/settings-shell-registry.js +7 -4
  41. package/lib/public/modules/settings-shell.js +17 -6
  42. package/lib/public/modules/sidebar-projects.js +23 -9
  43. package/lib/public/modules/split-pane-force-stop.js +47 -0
  44. package/lib/public/modules/split-pane-header-state.js +11 -1
  45. package/lib/public/modules/split-pane-renderer.js +2 -2
  46. package/lib/public/modules/split-view-workers.js +32 -17
  47. package/lib/public/modules/stt-settings.js +21 -4
  48. package/lib/public/modules/user-settings-layout.js +22 -18
  49. package/lib/public/modules/user-settings-provider-install.js +42 -0
  50. package/lib/public/modules/user-settings-providers.js +102 -0
  51. package/lib/public/modules/user-settings-voice.js +50 -41
  52. package/lib/public/modules/vendor-login-browser.js +1 -1
  53. package/lib/public/modules/vendor-login.js +72 -12
  54. package/lib/public/modules/worker-activity-indicator.js +49 -0
  55. package/lib/public/modules/worker-message-previews.js +47 -11
  56. package/lib/public/modules/worker-stop-controls.js +36 -0
  57. package/lib/public/modules/worktree-controls.js +86 -0
  58. package/lib/public/pane-assets/{app-SEMVZ4AY.js → app-PV3SEOTX.js} +1 -1
  59. package/lib/public/pane-assets/{chunk-C6P7PICY.js → chunk-2G6DB3ES.js} +1 -1
  60. package/lib/public/pane-assets/{chunk-BNNPJJVG.js → chunk-4WYURTPH.js} +1 -1
  61. package/lib/public/pane-assets/{chunk-E4VM2IDB.js → chunk-53KRXJDL.js} +1 -1
  62. package/lib/public/pane-assets/{chunk-4YE4RU27.js → chunk-AMZ7P526.js} +1 -1
  63. package/lib/public/pane-assets/chunk-BK2NRZFM.js +1 -0
  64. package/lib/public/pane-assets/{chunk-5UBN6GR7.js → chunk-D3WR2TYZ.js} +1 -1
  65. package/lib/public/pane-assets/chunk-E7D5A27I.js +1 -0
  66. package/lib/public/pane-assets/{chunk-CQNPEH2M.js → chunk-FB5B2WYF.js} +1 -1
  67. package/lib/public/pane-assets/chunk-FDBSE7BE.js +174 -0
  68. package/lib/public/pane-assets/{chunk-FK3YLGAP.js → chunk-FHB76KH2.js} +1 -1
  69. package/lib/public/pane-assets/{chunk-YR5GH3GI.js → chunk-FNGO6432.js} +1 -1
  70. package/lib/public/pane-assets/chunk-HF7AK56Q.js +1 -0
  71. package/lib/public/pane-assets/chunk-ILVRGTDF.js +1 -0
  72. package/lib/public/pane-assets/{chunk-QWQB2YRF.js → chunk-L3SFMSLP.js} +1 -1
  73. package/lib/public/pane-assets/{chunk-QBCM7QEA.js → chunk-PAY3A4JS.js} +1 -1
  74. package/lib/public/pane-assets/chunk-QVHC324H.js +1 -0
  75. package/lib/public/pane-assets/chunk-SFANEWZA.js +1 -0
  76. package/lib/public/pane-assets/{chunk-M5CWIIIT.js → chunk-SGMEO3P7.js} +1 -1
  77. package/lib/public/pane-assets/{chunk-YVVT3CTV.js → chunk-TK446XFF.js} +1 -1
  78. package/lib/public/pane-assets/{chunk-U7EHYRJZ.js → chunk-TU3AHPSN.js} +1 -1
  79. package/lib/public/pane-assets/chunk-US5VELGB.js +1 -0
  80. package/lib/public/pane-assets/{chunk-K4PAN77B.js → chunk-UVTS6GUM.js} +1 -1
  81. package/lib/public/pane-assets/{chunk-XYZGVYWW.js → chunk-X45WI2U7.js} +1 -1
  82. package/lib/public/pane-assets/manifest.json +401 -393
  83. package/lib/public/pane-assets/style-563D4UN6.css +1 -0
  84. package/lib/sdk-bridge.js +40 -6
  85. package/lib/sdk-force-stop.js +62 -0
  86. package/lib/sdk-message-processor.js +2 -1
  87. package/lib/server-worktree-controls.js +34 -0
  88. package/lib/server.js +11 -1
  89. package/lib/session-force-stop-support.js +15 -0
  90. package/lib/sessions.js +6 -1
  91. package/lib/terminal-manager.js +1 -1
  92. package/lib/terminal.js +13 -1
  93. package/lib/vendor-login-browser-flow.js +1 -1
  94. package/lib/worker-activity.js +26 -0
  95. package/lib/worker-message-previews.js +12 -7
  96. package/lib/worktree-branch.js +57 -0
  97. package/lib/worktree-identity.js +39 -0
  98. package/lib/worktree-preferences.js +25 -0
  99. package/lib/worktree-ref-transaction.js +36 -0
  100. package/lib/yoke/adapters/antigravity.js +3 -0
  101. package/lib/yoke/force-termination.js +26 -0
  102. package/lib/yoke/interface.js +14 -0
  103. package/lib/yoke/process-force-termination.js +24 -0
  104. package/lib/yoke/provider-install-recipes.js +20 -0
  105. package/lib/yoke/vendor-registry.js +20 -1
  106. package/package.json +1 -1
  107. package/lib/public/pane-assets/chunk-6AGSOAOD.js +0 -1
  108. package/lib/public/pane-assets/chunk-AATAIAXL.js +0 -1
  109. package/lib/public/pane-assets/chunk-D3CE5FZL.js +0 -1
  110. package/lib/public/pane-assets/chunk-GKSLBGVE.js +0 -168
  111. package/lib/public/pane-assets/chunk-J5PTSKJ6.js +0 -1
  112. package/lib/public/pane-assets/chunk-QPBO6ID4.js +0 -1
  113. package/lib/public/pane-assets/chunk-RA6VUJTY.js +0 -1
  114. package/lib/public/pane-assets/chunk-XOZUM5ET.js +0 -1
  115. 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
- return { 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 };
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();
@@ -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
- stopSession(session, ws && ws._clayUser);
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) session._pendingMessageDrainPaused = false;
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
- // Vendor login flow coordinator.
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
- return JSON.stringify([ws && ws._clayUser ? String(ws._clayUser.id) : null, vendor]);
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 = typeof refreshVendorAuthEverywhere === "function"
214
- ? refreshVendorAuthEverywhere(flow.vendor, flow.linuxUser || null)
215
- : refreshVendorAuth(flow.vendor, flow.linuxUser || null);
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 = startFlow(ws, vendor, msg);
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 };