@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.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 (162) hide show
  1. package/docs/desktop-browser-cli.md +33 -0
  2. package/openapi.yaml +17 -5
  3. package/package.json +1 -1
  4. package/scripts/smoke-desktop-browser-cli.ts +279 -0
  5. package/src/__tests__/config-schema.test.ts +1 -1
  6. package/src/__tests__/config-sounds-sync.test.ts +23 -0
  7. package/src/__tests__/conversation-error.test.ts +4 -1
  8. package/src/__tests__/conversation-runtime-assembly.test.ts +6 -4
  9. package/src/__tests__/external-plugin-loader.test.ts +52 -0
  10. package/src/__tests__/headless-browser-mode.test.ts +48 -0
  11. package/src/__tests__/host-cu-proxy.test.ts +116 -18
  12. package/src/__tests__/list-all-apps.test.ts +40 -8
  13. package/src/__tests__/mcp-auth-routes.test.ts +99 -0
  14. package/src/__tests__/mcp-client-auth.test.ts +54 -0
  15. package/src/__tests__/mcp-health-check.test.ts +32 -32
  16. package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
  17. package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
  18. package/src/__tests__/skills.test.ts +10 -0
  19. package/src/__tests__/user-plugin-loader.test.ts +21 -0
  20. package/src/acp/session-snapshot.ts +260 -0
  21. package/src/apps/app-store.ts +6 -5
  22. package/src/browser/operations.ts +2 -1
  23. package/src/browser/types.ts +7 -0
  24. package/src/browser/virtual-desktop-target.ts +34 -0
  25. package/src/calls/__tests__/progress-narration.test.ts +23 -0
  26. package/src/calls/__tests__/voice-control-protocol.test.ts +7 -0
  27. package/src/calls/__tests__/voice-session-bridge.test.ts +62 -0
  28. package/src/calls/progress-narration.ts +15 -2
  29. package/src/calls/voice-control-protocol.ts +34 -4
  30. package/src/calls/voice-session-bridge.ts +23 -0
  31. package/src/cli/commands/__tests__/browser.test.ts +84 -3
  32. package/src/cli/commands/browser.help.ts +43 -0
  33. package/src/cli/commands/browser.ts +52 -8
  34. package/src/cli/commands/plugins.ts +16 -5
  35. package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
  36. package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
  37. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
  38. package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
  39. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
  40. package/src/cli/lib/bundled-marketplace.json +13 -0
  41. package/src/cli/lib/install-from-github.ts +17 -18
  42. package/src/cli/lib/install-from-platform.ts +14 -9
  43. package/src/cli/lib/list-installed-plugins.ts +21 -35
  44. package/src/cli/lib/plugin-details.ts +21 -8
  45. package/src/cli/lib/uninstall-plugin.ts +59 -1
  46. package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
  47. package/src/config/bundled-skills/acp/SKILL.md +1 -1
  48. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  49. package/src/config/bundled-skills/computer-use/SKILL.md +30 -0
  50. package/src/config/bundled-skills/computer-use/TOOLS.json +2 -2
  51. package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
  52. package/src/config/feature-flag-registry.json +2 -2
  53. package/src/config/schemas/__tests__/voice.test.ts +2 -2
  54. package/src/config/schemas/mcp.ts +9 -7
  55. package/src/config/schemas/monitoring.ts +9 -0
  56. package/src/config/schemas/voice.ts +2 -2
  57. package/src/config/skills.ts +13 -21
  58. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
  59. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
  60. package/src/daemon/conversation-client-surface.ts +26 -0
  61. package/src/daemon/conversation-runtime-assembly.ts +1 -1
  62. package/src/daemon/conversation-tool-setup.ts +31 -29
  63. package/src/daemon/host-cu-proxy.ts +51 -26
  64. package/src/daemon/host-proxy-preactivation.ts +2 -0
  65. package/src/daemon/mcp-reload-service.ts +7 -0
  66. package/src/daemon/message-types/sync.ts +1 -0
  67. package/src/daemon/providers-setup.ts +4 -0
  68. package/src/desktop/__tests__/fake-desktop.ts +1 -0
  69. package/src/desktop/desktop-automation-lease.test.ts +208 -0
  70. package/src/desktop/desktop-automation-lease.ts +270 -0
  71. package/src/desktop/desktop-browser-client.test.ts +413 -0
  72. package/src/desktop/desktop-browser-client.ts +430 -0
  73. package/src/desktop/desktop-browser-cursor.ts +40 -0
  74. package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
  75. package/src/desktop/desktop-browser-endpoint.ts +145 -0
  76. package/src/desktop/desktop-browser-operations.ts +130 -0
  77. package/src/desktop/desktop-chrome-session.test.ts +48 -0
  78. package/src/desktop/desktop-chrome-session.ts +58 -8
  79. package/src/desktop/desktop-dependencies.test.ts +29 -0
  80. package/src/desktop/desktop-dependencies.ts +26 -5
  81. package/src/desktop/desktop-display.ts +9 -0
  82. package/src/desktop/desktop-panel-config.ts +14 -1
  83. package/src/desktop/desktop-session-manager.test.ts +86 -3
  84. package/src/desktop/desktop-session-manager.ts +144 -24
  85. package/src/desktop/desktop-stream-bridge.ts +1 -1
  86. package/src/desktop/virtual-desktop-feature.ts +18 -0
  87. package/src/desktop/virtual-desktop-platform.test.ts +61 -0
  88. package/src/i18n/__tests__/i18n.test.ts +9 -5
  89. package/src/i18n/messages.ts +13 -4
  90. package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
  91. package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
  92. package/src/ipc/assistant-server.ts +15 -1
  93. package/src/ipc/cli-client.ts +13 -5
  94. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +22 -0
  95. package/src/live-voice/__tests__/live-voice-events.test.ts +3 -2
  96. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
  97. package/src/live-voice/__tests__/protocol.test.ts +11 -3
  98. package/src/live-voice/__tests__/session-controls.test.ts +34 -0
  99. package/src/live-voice/live-voice-metrics.ts +9 -0
  100. package/src/live-voice/live-voice-session.ts +3 -0
  101. package/src/live-voice/protocol.ts +10 -1
  102. package/src/live-voice/session-controls.ts +28 -0
  103. package/src/mcp/__tests__/credential-target.test.ts +59 -0
  104. package/src/mcp/__tests__/effective-config.test.ts +11 -0
  105. package/src/mcp/__tests__/manager-state.test.ts +128 -0
  106. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
  107. package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
  108. package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
  109. package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
  110. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
  111. package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
  112. package/src/mcp/client.ts +64 -16
  113. package/src/mcp/credential-target.ts +105 -0
  114. package/src/mcp/effective-config.ts +42 -15
  115. package/src/mcp/manager.ts +61 -8
  116. package/src/mcp/mcp-auth-orchestrator.ts +17 -4
  117. package/src/mcp/mcp-auth-state.ts +38 -6
  118. package/src/mcp/mcp-oauth-provider.ts +101 -33
  119. package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
  120. package/src/monitoring/mount-watch.ts +348 -0
  121. package/src/monitoring/worker.ts +9 -0
  122. package/src/notifications/__tests__/decision-engine.test.ts +175 -0
  123. package/src/notifications/decision-engine.ts +43 -8
  124. package/src/oauth/seed-providers.ts +11 -0
  125. package/src/onboarding/checkin-event.ts +2 -1
  126. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
  127. package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
  128. package/src/plugins/external-plugin-loader.ts +49 -86
  129. package/src/plugins/installed-plugin-dirs.ts +6 -4
  130. package/src/plugins/mcp-servers.ts +9 -15
  131. package/src/plugins/mtime-cache.ts +2 -1
  132. package/src/plugins/user-loader.ts +2 -1
  133. package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
  134. package/src/providers/inference/endpoint-probe.ts +27 -12
  135. package/src/providers/openai/responses-provider.ts +9 -2
  136. package/src/providers/opencode/client.test.ts +167 -0
  137. package/src/providers/opencode/client.ts +73 -4
  138. package/src/runtime/AGENTS.md +4 -0
  139. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
  140. package/src/runtime/http-server.ts +6 -6
  141. package/src/runtime/routes/__tests__/acp-routes.test.ts +19 -0
  142. package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
  143. package/src/runtime/routes/acp-routes.ts +17 -161
  144. package/src/runtime/routes/browser-context.ts +39 -0
  145. package/src/runtime/routes/browser-routes.ts +16 -47
  146. package/src/runtime/routes/browser-tabs-routes.ts +72 -29
  147. package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
  148. package/src/runtime/routes/desktop-setup-routes.ts +7 -5
  149. package/src/runtime/routes/mcp-auth-routes.ts +106 -94
  150. package/src/runtime/routes/plugins-routes.ts +14 -1
  151. package/src/runtime/sync/resource-sync-events.ts +4 -0
  152. package/src/tools/acp/status.test.ts +276 -21
  153. package/src/tools/acp/status.ts +98 -32
  154. package/src/tools/browser/browser-execution.ts +44 -13
  155. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
  156. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
  157. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
  158. package/src/tools/computer-use/definitions.ts +2 -3
  159. package/src/util/abort-reasons.ts +12 -1
  160. package/src/util/plugin-manifest.ts +203 -0
  161. package/src/desktop/desktop-feature.test.ts +0 -18
  162. package/src/desktop/desktop-feature.ts +0 -19
@@ -12,16 +12,30 @@ import { join } from "node:path";
12
12
  import { readAvatarState } from "../avatar/avatar-manifest.js";
13
13
  import { resolveNotificationAccentHex } from "../avatar/notification-avatar.js";
14
14
  import { getIsContainerized } from "../config/env-registry.js";
15
+ import { connectCdpWsTransport } from "../tools/browser/cdp-client/cdp-inspect/ws-transport.js";
15
16
  import { terminateProcessTree } from "../util/host-process.js";
16
17
  import { getLogger } from "../util/logger.js";
17
18
  import { getDataDir } from "../util/platform.js";
18
19
  import { sleep } from "../util/retry.js";
20
+ import { DesktopBrowserClient } from "./desktop-browser-client.js";
21
+ import {
22
+ allocateDesktopDebugPort,
23
+ desktopChromeArguments,
24
+ discoverDesktopBrowser,
25
+ findDesktopBrowserPid,
26
+ } from "./desktop-browser-endpoint.js";
19
27
  import { writeDesktopChromePolicy } from "./desktop-chrome-policy.js";
20
- import { shouldRestoreDesktopChromeSession } from "./desktop-chrome-session.js";
28
+ import { configureDesktopChromeFrame } from "./desktop-chrome-session.js";
21
29
  import {
22
30
  desktopChromePath,
23
31
  resolveDesktopBinaries,
24
32
  } from "./desktop-dependencies.js";
33
+ import {
34
+ DESKTOP_DISPLAY,
35
+ DESKTOP_HEIGHT,
36
+ DESKTOP_OVERRIDABLE_PARAMETERS,
37
+ DESKTOP_WIDTH,
38
+ } from "./desktop-display.js";
25
39
  import { writeDesktopPanelConfig } from "./desktop-panel-config.js";
26
40
  import { renderCurrentDesktopWallpaper } from "./desktop-wallpaper.js";
27
41
  import { DESKTOP_WINDOW_DRAG_SCRIPT } from "./desktop-window-drag.js";
@@ -30,10 +44,7 @@ import { writeDesktopWindowTheme } from "./desktop-window-theme.js";
30
44
 
31
45
  const log = getLogger("desktop-session");
32
46
 
33
- const DESKTOP_DISPLAY = ":99";
34
47
  export const DESKTOP_VNC_PORT = 5999;
35
- const DESKTOP_WIDTH = 1600;
36
- const DESKTOP_HEIGHT = 900;
37
48
  const DESKTOP_GEOMETRY = `${DESKTOP_WIDTH}x${DESKTOP_HEIGHT}`;
38
49
  const DESKTOP_LINGER_MS = 5 * 60_000;
39
50
  const VNC_READY_DEADLINE_MS = 10_000;
@@ -153,6 +164,7 @@ type ViewerSlotResult =
153
164
  | { readonly ok: false; readonly loss: DesktopLoss };
154
165
 
155
166
  interface DesktopSessionManagerOptions {
167
+ readonly allocateDebugPort?: () => Promise<number>;
156
168
  readonly spawn?: (
157
169
  role: DesktopChildRole,
158
170
  request: DesktopSpawnRequest,
@@ -194,6 +206,7 @@ export class DesktopSessionManager {
194
206
  /** Bumped on every teardown so an in-flight start notices it lost its tree. */
195
207
  private generation = 0;
196
208
  private viewer: DesktopViewer | null = null;
209
+ private automation: DesktopViewer | null = null;
197
210
  private lingerTimer: ReturnType<typeof setTimeout> | null = null;
198
211
  private ingressClosed = false;
199
212
  /** Resolved for the current tree, and read again when the dock comes up. */
@@ -211,6 +224,70 @@ export class DesktopSessionManager {
211
224
  refreshQueued: boolean;
212
225
  } | null = null;
213
226
 
227
+ private debugPort?: number;
228
+ private browserStarting: Promise<void> | null = null;
229
+ private readonly allocateDebugPort: () => Promise<number>;
230
+ readonly browser = new DesktopBrowserClient(async (signal) => {
231
+ if (!this.automation) {
232
+ throw new Error("Desktop browser requires an automation slot");
233
+ }
234
+ const generation = this.generation;
235
+ const owner = this.automation;
236
+ let child = this.children.get("browser");
237
+ let pid = child?.pid;
238
+ if (!pid && this.debugPort) {
239
+ pid = await findDesktopBrowserPid(
240
+ await this.resolveChromePath(),
241
+ this.profileDir,
242
+ this.debugPort,
243
+ );
244
+ }
245
+ if (this.generation !== generation || this.automation !== owner) {
246
+ throw new Error("Desktop Chrome session changed. Observe again.");
247
+ }
248
+ signal.throwIfAborted();
249
+ if (!pid) {
250
+ await this.ensureBrowser(this.childEnv(), generation);
251
+ child = this.children.get("browser");
252
+ pid = child?.pid;
253
+ }
254
+ const port = this.debugPort;
255
+ const isCurrent = () =>
256
+ this.generation === generation &&
257
+ this.automation === owner &&
258
+ (!child || this.children.get("browser") === child);
259
+ if (!pid || !port) {
260
+ throw new Error(
261
+ "Desktop Chrome is unavailable. Open the Desktop modal to inspect it, then retry the browser command.",
262
+ );
263
+ }
264
+ signal = AbortSignal.any([signal, AbortSignal.timeout(5_000)]);
265
+ const deadline = Date.now() + 5_000;
266
+ while (true) {
267
+ signal.throwIfAborted();
268
+ if (!isCurrent()) {
269
+ throw new Error("Desktop Chrome session changed. Observe again.");
270
+ }
271
+ try {
272
+ const url = await discoverDesktopBrowser(pid, port, signal);
273
+ const transport = await connectCdpWsTransport(url, {
274
+ signal,
275
+ connectTimeoutMs: 3000,
276
+ });
277
+ if (!isCurrent() || signal.aborted) {
278
+ transport.dispose();
279
+ throw new Error("Desktop Chrome session changed. Observe again.");
280
+ }
281
+ return transport;
282
+ } catch (error) {
283
+ if (Date.now() >= deadline || signal.aborted) {
284
+ throw error;
285
+ }
286
+ await sleep(100);
287
+ }
288
+ }
289
+ });
290
+
214
291
  private readonly spawn: NonNullable<DesktopSessionManagerOptions["spawn"]>;
215
292
  private readonly which: NonNullable<DesktopSessionManagerOptions["which"]>;
216
293
  private readonly probeVncPort: NonNullable<
@@ -235,6 +312,8 @@ export class DesktopSessionManager {
235
312
  private readonly writeWindowManagerConfig: (configDir: string) => string;
236
313
 
237
314
  constructor(options: DesktopSessionManagerOptions = {}) {
315
+ this.allocateDebugPort =
316
+ options.allocateDebugPort ?? allocateDesktopDebugPort;
238
317
  this.spawn = options.spawn ?? spawnDetached;
239
318
  this.which = options.which ?? Bun.which;
240
319
  this.probeVncPort = options.probeVncPort ?? probeLoopbackPort;
@@ -308,7 +387,29 @@ export class DesktopSessionManager {
308
387
  return;
309
388
  }
310
389
  this.viewer = null;
311
- if (this.running || this.starting) {
390
+ if (!this.automation && (this.running || this.starting)) {
391
+ this.armLinger();
392
+ }
393
+ }
394
+
395
+ acquireAutomationSlot(owner: DesktopViewer): ViewerSlotResult {
396
+ if (this.ingressClosed) {
397
+ return { ok: false, loss: SHUTTING_DOWN_LOSS };
398
+ }
399
+ if (this.automation) {
400
+ return { ok: false, loss: BUSY_LOSS };
401
+ }
402
+ this.automation = owner;
403
+ this.clearLinger();
404
+ return { ok: true };
405
+ }
406
+
407
+ releaseAutomationSlot(owner: DesktopViewer): void {
408
+ if (this.automation !== owner) {
409
+ return;
410
+ }
411
+ this.automation = null;
412
+ if (!this.viewer && (this.running || this.starting)) {
312
413
  this.armLinger();
313
414
  }
314
415
  }
@@ -393,7 +494,7 @@ export class DesktopSessionManager {
393
494
  this.running = true;
394
495
  log.info({ display: DESKTOP_DISPLAY }, "Desktop started");
395
496
  void this.refreshWallpaper(env, generation);
396
- void this.startBrowser(env, generation);
497
+ void this.ensureBrowser(env, generation);
397
498
  }
398
499
 
399
500
  private async refreshWallpaper(
@@ -455,18 +556,42 @@ export class DesktopSessionManager {
455
556
  }
456
557
 
457
558
  /** Launch Chrome and its dock once the X server is ready. */
559
+ private ensureBrowser(
560
+ env: Record<string, string>,
561
+ generation: number,
562
+ ): Promise<void> {
563
+ if (this.children.has("browser")) {
564
+ return Promise.resolve();
565
+ }
566
+ this.browserStarting ??= this.startBrowser(env, generation).finally(() => {
567
+ this.browserStarting = null;
568
+ });
569
+ return this.browserStarting;
570
+ }
571
+
458
572
  private async startBrowser(
459
573
  env: Record<string, string>,
460
574
  generation: number,
461
575
  ): Promise<void> {
462
576
  try {
463
577
  const executable = await this.resolveChromePath();
464
- if (this.generation !== generation) {
578
+ const debugPort = this.debugPort ?? (await this.allocateDebugPort());
579
+ if (this.generation !== generation || this.children.has("browser")) {
465
580
  return;
466
581
  }
467
582
  mkdirSync(this.profileDir, { recursive: true });
583
+ this.debugPort = debugPort;
584
+ try {
585
+ configureDesktopChromeFrame(this.profileDir);
586
+ } catch (err) {
587
+ log.warn({ err }, "Desktop Chrome frame could not be configured");
588
+ }
468
589
  this.startPanel(executable, env);
469
- this.launch("browser", browserCommand(executable, this.profileDir), env);
590
+ this.launch(
591
+ "browser",
592
+ [executable, ...desktopChromeArguments(this.profileDir, debugPort)],
593
+ env,
594
+ );
470
595
  } catch (err) {
471
596
  log.warn({ err }, "Desktop browser failed to launch");
472
597
  if (this.generation === generation) {
@@ -491,6 +616,7 @@ export class DesktopSessionManager {
491
616
  configDir: this.panelConfigDir,
492
617
  chromiumPath,
493
618
  chromiumProfileDir: this.profileDir,
619
+ debugPort: this.debugPort,
494
620
  terminalPath: binaries.terminal,
495
621
  });
496
622
  this.launch("panel", [binaries.panelSession, "--", binaries.panel], {
@@ -577,6 +703,7 @@ export class DesktopSessionManager {
577
703
  return;
578
704
  }
579
705
  if (role === "browser") {
706
+ this.browser.dispose();
580
707
  log.info({ outcome }, "Desktop browser exited");
581
708
  return;
582
709
  }
@@ -599,6 +726,8 @@ export class DesktopSessionManager {
599
726
  private teardown(loss?: DesktopLoss): Promise<void> {
600
727
  this.clearLinger();
601
728
  this.generation += 1;
729
+ this.browser.dispose();
730
+ this.debugPort = undefined;
602
731
  this.running = false;
603
732
  this.binaries = null;
604
733
  this.panelStarted = false;
@@ -613,7 +742,12 @@ export class DesktopSessionManager {
613
742
  const children = new Map(this.children);
614
743
  this.children.clear();
615
744
  const viewer = this.viewer;
745
+ const automation = this.automation;
616
746
  this.viewer = null;
747
+ this.automation = null;
748
+ if (automation && loss) {
749
+ automation.onDesktopLost(loss);
750
+ }
617
751
  if (viewer && loss) {
618
752
  viewer.onDesktopLost(loss);
619
753
  }
@@ -733,6 +867,8 @@ function xServerCommand(executable: string): string[] {
733
867
  "None",
734
868
  "-rfbport",
735
869
  String(DESKTOP_VNC_PORT),
870
+ "-AllowOverride",
871
+ DESKTOP_OVERRIDABLE_PARAMETERS.join(","),
736
872
  "-geometry",
737
873
  DESKTOP_GEOMETRY,
738
874
  // Keep the dock and wallpaper anchored to a stable display.
@@ -744,22 +880,6 @@ function xServerCommand(executable: string): string[] {
744
880
  ];
745
881
  }
746
882
 
747
- function browserCommand(executable: string, profileDir: string): string[] {
748
- // Explicit window bounds override Chrome's maximized startup state.
749
- return [
750
- executable,
751
- "--no-sandbox",
752
- "--no-first-run",
753
- "--disable-dev-shm-usage",
754
- // Crash recovery requires both flags; clean exits use normal startup.
755
- ...(shouldRestoreDesktopChromeSession(profileDir)
756
- ? ["--restore-last-session", "--hide-crash-restore-bubble"]
757
- : []),
758
- "--start-maximized",
759
- `--user-data-dir=${profileDir}`,
760
- ];
761
- }
762
-
763
883
  // ---------------------------------------------------------------------------
764
884
  // Default collaborators
765
885
  // ---------------------------------------------------------------------------
@@ -178,7 +178,7 @@ export class DesktopStreamBridge {
178
178
  if (!this.isEnabled()) {
179
179
  this.fail(
180
180
  DESKTOP_CLOSE.unavailable,
181
- "Desktop is not available on this assistant",
181
+ "Virtual desktop is available only on enabled platform-hosted assistants",
182
182
  );
183
183
  return false;
184
184
  }
@@ -0,0 +1,18 @@
1
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
2
+ import { getIsContainerized, getIsPlatform } from "../config/env-registry.js";
3
+ import type { AssistantConfig } from "../config/schema.js";
4
+
5
+ const VIRTUAL_DESKTOP_FLAG = "assistant-desktop" as const;
6
+
7
+ /** Gates desktop streaming and control to enabled, platform-hosted containers. */
8
+ export function isVirtualDesktopEnabled(
9
+ config: AssistantConfig,
10
+ containerized: boolean = getIsContainerized(),
11
+ platformHosted: boolean = getIsPlatform(),
12
+ ): boolean {
13
+ return (
14
+ platformHosted &&
15
+ containerized &&
16
+ isAssistantFeatureFlagEnabled(VIRTUAL_DESKTOP_FLAG, config)
17
+ );
18
+ }
@@ -0,0 +1,61 @@
1
+ import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
2
+
3
+ import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
4
+ import { shouldUseVirtualDesktopBrowser } from "../browser/virtual-desktop-target.js";
5
+ import { executeDesktopBrowserOperation } from "./desktop-browser-operations.js";
6
+ import { desktopDependencyInstaller } from "./desktop-dependencies.js";
7
+
8
+ const originalPlatform = process.env.IS_PLATFORM;
9
+ const originalContainerized = process.env.IS_CONTAINERIZED;
10
+ const context = {
11
+ workingDir: "/tmp",
12
+ conversationId: "conv-platform-gate",
13
+ sourceActorPrincipalId: "user-123",
14
+ trustClass: "guardian" as const,
15
+ transportInterface: "web" as const,
16
+ clientOs: "web" as const,
17
+ };
18
+ let status: ReturnType<
19
+ typeof spyOn<typeof desktopDependencyInstaller, "getStatus">
20
+ >;
21
+
22
+ beforeEach(() => {
23
+ process.env.IS_CONTAINERIZED = "true";
24
+ process.env.IS_PLATFORM = "false";
25
+ setOverridesForTesting({ "assistant-desktop": true });
26
+ status = spyOn(desktopDependencyInstaller, "getStatus").mockReturnValue({
27
+ state: "ready",
28
+ });
29
+ });
30
+
31
+ afterEach(() => {
32
+ status.mockRestore();
33
+ setOverridesForTesting({});
34
+ if (originalPlatform === undefined) {
35
+ delete process.env.IS_PLATFORM;
36
+ } else {
37
+ process.env.IS_PLATFORM = originalPlatform;
38
+ }
39
+ if (originalContainerized === undefined) {
40
+ delete process.env.IS_CONTAINERIZED;
41
+ } else {
42
+ process.env.IS_CONTAINERIZED = originalContainerized;
43
+ }
44
+ });
45
+
46
+ test("self-hosted Docker cannot automatically select or explicitly control virtual Chrome", async () => {
47
+ expect(shouldUseVirtualDesktopBrowser(undefined, {}, context)).toBe(false);
48
+ await expect(
49
+ executeDesktopBrowserOperation("snapshot", {}, context),
50
+ ).rejects.toThrow("only on enabled platform-hosted assistants");
51
+ expect(status).not.toHaveBeenCalled();
52
+ });
53
+
54
+ test("platform web conversations select virtual Chrome before and during installation", () => {
55
+ process.env.IS_PLATFORM = "true";
56
+ expect(shouldUseVirtualDesktopBrowser(undefined, {}, context)).toBe(true);
57
+ status.mockReturnValue({ state: "installing" });
58
+ expect(shouldUseVirtualDesktopBrowser(undefined, {}, context)).toBe(true);
59
+ status.mockReturnValue({ state: "required" });
60
+ expect(shouldUseVirtualDesktopBrowser(undefined, {}, context)).toBe(true);
61
+ });
@@ -35,9 +35,7 @@ describe("negotiateLocale", () => {
35
35
 
36
36
  describe("localeFromAcceptLanguage", () => {
37
37
  test("honors quality values", () => {
38
- expect(localeFromAcceptLanguage("fr;q=0.8, es;q=0.9, en;q=0.5")).toBe(
39
- "es",
40
- );
38
+ expect(localeFromAcceptLanguage("fr;q=0.8, es;q=0.9, en;q=0.5")).toBe("es");
41
39
  });
42
40
 
43
41
  test("defaults on missing or empty", () => {
@@ -58,7 +56,7 @@ describe("catalog completeness", () => {
58
56
  });
59
57
 
60
58
  describe("t", () => {
61
- test("resolves generating and untitled per locale", () => {
59
+ test("resolves message keys per locale", () => {
62
60
  expect(t(MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING, "en")).toBe(
63
61
  "Generating title...",
64
62
  );
@@ -68,13 +66,19 @@ describe("t", () => {
68
66
  expect(t(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED, "es")).toBe(
69
67
  "Sin título",
70
68
  );
69
+ expect(
70
+ t(MESSAGE_KEYS.PLUGIN_MCP_OAUTH_CREDENTIALS_UNCHECKED, "es"),
71
+ ).toContain("almacenamiento de credenciales");
71
72
  });
72
73
  });
73
74
 
74
75
  describe("resolveConversationTitle", () => {
75
76
  test("resolves a stored key and an empty title", () => {
76
77
  expect(
77
- resolveConversationTitle(MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING, "zh"),
78
+ resolveConversationTitle(
79
+ MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING,
80
+ "zh",
81
+ ),
78
82
  ).toBe("标题生成中...");
79
83
  expect(resolveConversationTitle(null, "es")).toBe("Sin título");
80
84
  expect(resolveConversationTitle("")).toBe("Untitled Conversation");
@@ -11,14 +11,13 @@
11
11
  * a user title may be in any language.
12
12
  */
13
13
 
14
- import {
15
- DEFAULT_LOCALE,
16
- type SupportedLocale,
17
- } from "./locales.js";
14
+ import { DEFAULT_LOCALE, type SupportedLocale } from "./locales.js";
18
15
 
19
16
  export const MESSAGE_KEYS = {
20
17
  CONVERSATION_TITLE_GENERATING: "conversation.title.generating",
21
18
  CONVERSATION_TITLE_UNTITLED: "conversation.title.untitled",
19
+ PLUGIN_MCP_OAUTH_CREDENTIALS_UNCHECKED:
20
+ "plugin.uninstall.mcp_oauth_credentials_unchecked",
22
21
  } as const;
23
22
 
24
23
  export type MessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];
@@ -30,22 +29,32 @@ export const MESSAGE_CATALOGS: Record<
30
29
  en: {
31
30
  "conversation.title.generating": "Generating title...",
32
31
  "conversation.title.untitled": "Untitled Conversation",
32
+ "plugin.uninstall.mcp_oauth_credentials_unchecked":
33
+ "Credential storage is unavailable, so historical plugin MCP OAuth credentials could not be checked.",
33
34
  },
34
35
  es: {
35
36
  "conversation.title.generating": "Generando título...",
36
37
  "conversation.title.untitled": "Sin título",
38
+ "plugin.uninstall.mcp_oauth_credentials_unchecked":
39
+ "El almacenamiento de credenciales no está disponible, por lo que no se pudieron comprobar las credenciales históricas de OAuth de MCP del plugin.",
37
40
  },
38
41
  ru: {
39
42
  "conversation.title.generating": "Создание названия...",
40
43
  "conversation.title.untitled": "Без названия",
44
+ "plugin.uninstall.mcp_oauth_credentials_unchecked":
45
+ "Хранилище учетных данных недоступно, поэтому не удалось проверить сохраненные учетные данные OAuth MCP плагина.",
41
46
  },
42
47
  zh: {
43
48
  "conversation.title.generating": "标题生成中...",
44
49
  "conversation.title.untitled": "无标题",
50
+ "plugin.uninstall.mcp_oauth_credentials_unchecked":
51
+ "凭据存储不可用,因此无法检查该插件之前的 MCP OAuth 凭据。",
45
52
  },
46
53
  "zh-TW": {
47
54
  "conversation.title.generating": "標題產生中...",
48
55
  "conversation.title.untitled": "未命名",
56
+ "plugin.uninstall.mcp_oauth_credentials_unchecked":
57
+ "憑證儲存空間無法使用,因此無法檢查此外掛程式先前的 MCP OAuth 憑證。",
49
58
  },
50
59
  };
51
60