@vellumai/assistant 0.12.2-staging.1 → 0.12.2-staging.3

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 (194) hide show
  1. package/docs/desktop-browser-cli.md +33 -0
  2. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  3. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  4. package/openapi.yaml +50 -4
  5. package/package.json +1 -1
  6. package/scripts/smoke-desktop-browser-cli.ts +279 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
  8. package/src/__tests__/config-schema.test.ts +1 -1
  9. package/src/__tests__/config-sounds-sync.test.ts +23 -0
  10. package/src/__tests__/conversation-error.test.ts +4 -1
  11. package/src/__tests__/conversation-runtime-assembly.test.ts +33 -3
  12. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  13. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  14. package/src/__tests__/credential-routes.test.ts +38 -0
  15. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  16. package/src/__tests__/cu-unified-flow.test.ts +6 -2
  17. package/src/__tests__/external-plugin-loader.test.ts +52 -0
  18. package/src/__tests__/headless-browser-mode.test.ts +48 -0
  19. package/src/__tests__/host-cu-proxy.test.ts +185 -30
  20. package/src/__tests__/list-all-apps.test.ts +40 -8
  21. package/src/__tests__/mcp-auth-routes.test.ts +99 -0
  22. package/src/__tests__/mcp-client-auth.test.ts +54 -0
  23. package/src/__tests__/mcp-health-check.test.ts +32 -32
  24. package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
  25. package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
  26. package/src/__tests__/oauth-commands-routes.test.ts +55 -0
  27. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  28. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  29. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  30. package/src/__tests__/skills.test.ts +10 -0
  31. package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
  32. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  33. package/src/__tests__/user-plugin-loader.test.ts +21 -0
  34. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  35. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  36. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  37. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  38. package/src/acp/acp-claude-oauth.ts +328 -14
  39. package/src/acp/acp-credentials.ts +19 -0
  40. package/src/acp/claude-token-refresh.ts +150 -0
  41. package/src/acp/prepare-agent-env.ts +21 -6
  42. package/src/apps/app-store.ts +6 -5
  43. package/src/browser/operations.ts +2 -1
  44. package/src/browser/types.ts +7 -0
  45. package/src/browser/virtual-desktop-target.ts +34 -0
  46. package/src/calls/__tests__/progress-narration.test.ts +23 -0
  47. package/src/calls/__tests__/voice-control-protocol.test.ts +69 -0
  48. package/src/calls/__tests__/voice-session-bridge.test.ts +81 -0
  49. package/src/calls/progress-narration.ts +15 -2
  50. package/src/calls/voice-control-protocol.ts +132 -0
  51. package/src/calls/voice-session-bridge.ts +56 -26
  52. package/src/calls/voice-triage-escalate.ts +3 -3
  53. package/src/cli/commands/__tests__/browser.test.ts +84 -3
  54. package/src/cli/commands/browser.help.ts +43 -0
  55. package/src/cli/commands/browser.ts +52 -8
  56. package/src/cli/commands/oauth/status.ts +5 -0
  57. package/src/cli/commands/plugins.ts +16 -5
  58. package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
  59. package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
  60. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
  61. package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
  62. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
  63. package/src/cli/lib/bundled-marketplace.json +13 -0
  64. package/src/cli/lib/install-from-github.ts +17 -18
  65. package/src/cli/lib/install-from-platform.ts +14 -9
  66. package/src/cli/lib/list-installed-plugins.ts +21 -35
  67. package/src/cli/lib/plugin-details.ts +21 -8
  68. package/src/cli/lib/uninstall-plugin.ts +59 -1
  69. package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
  70. package/src/config/bundled-skills/computer-use/SKILL.md +43 -4
  71. package/src/config/bundled-skills/computer-use/TOOLS.json +3 -3
  72. package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
  73. package/src/config/feature-flag-registry.json +11 -3
  74. package/src/config/loader.ts +1 -0
  75. package/src/config/schemas/__tests__/voice.test.ts +2 -2
  76. package/src/config/schemas/mcp.ts +9 -7
  77. package/src/config/schemas/monitoring.ts +9 -0
  78. package/src/config/schemas/services.ts +10 -0
  79. package/src/config/schemas/voice.ts +2 -2
  80. package/src/config/skills.ts +13 -21
  81. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
  82. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
  83. package/src/daemon/conversation-client-surface.ts +26 -0
  84. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  85. package/src/daemon/conversation-surfaces.ts +6 -1
  86. package/src/daemon/conversation-tool-setup.ts +37 -43
  87. package/src/daemon/host-cu-proxy.ts +94 -31
  88. package/src/daemon/host-proxy-preactivation.ts +2 -0
  89. package/src/daemon/mcp-reload-service.ts +7 -0
  90. package/src/daemon/message-types/sync.ts +1 -0
  91. package/src/daemon/providers-setup.ts +4 -0
  92. package/src/desktop/__tests__/fake-desktop.ts +1 -0
  93. package/src/desktop/desktop-automation-lease.test.ts +208 -0
  94. package/src/desktop/desktop-automation-lease.ts +270 -0
  95. package/src/desktop/desktop-browser-client.test.ts +413 -0
  96. package/src/desktop/desktop-browser-client.ts +430 -0
  97. package/src/desktop/desktop-browser-cursor.ts +40 -0
  98. package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
  99. package/src/desktop/desktop-browser-endpoint.ts +145 -0
  100. package/src/desktop/desktop-browser-operations.ts +130 -0
  101. package/src/desktop/desktop-chrome-session.test.ts +48 -0
  102. package/src/desktop/desktop-chrome-session.ts +58 -8
  103. package/src/desktop/desktop-dependencies.test.ts +29 -0
  104. package/src/desktop/desktop-dependencies.ts +26 -5
  105. package/src/desktop/desktop-display.ts +9 -0
  106. package/src/desktop/desktop-panel-config.ts +14 -1
  107. package/src/desktop/desktop-session-manager.test.ts +86 -3
  108. package/src/desktop/desktop-session-manager.ts +144 -24
  109. package/src/desktop/desktop-stream-bridge.ts +1 -1
  110. package/src/desktop/virtual-desktop-feature.ts +18 -0
  111. package/src/desktop/virtual-desktop-platform.test.ts +61 -0
  112. package/src/i18n/__tests__/i18n.test.ts +9 -5
  113. package/src/i18n/messages.ts +13 -4
  114. package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
  115. package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
  116. package/src/ipc/assistant-server.ts +15 -1
  117. package/src/ipc/cli-client.ts +13 -5
  118. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +143 -0
  119. package/src/live-voice/__tests__/live-voice-events.test.ts +9 -7
  120. package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
  121. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
  122. package/src/live-voice/__tests__/protocol.test.ts +47 -0
  123. package/src/live-voice/__tests__/session-controls.test.ts +113 -0
  124. package/src/live-voice/live-voice-metrics.ts +9 -0
  125. package/src/live-voice/live-voice-session.ts +88 -7
  126. package/src/live-voice/protocol.ts +69 -0
  127. package/src/live-voice/session-controls.ts +139 -0
  128. package/src/mcp/__tests__/credential-target.test.ts +59 -0
  129. package/src/mcp/__tests__/effective-config.test.ts +11 -0
  130. package/src/mcp/__tests__/manager-state.test.ts +128 -0
  131. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
  132. package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
  133. package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
  134. package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
  135. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
  136. package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
  137. package/src/mcp/client.ts +64 -16
  138. package/src/mcp/credential-target.ts +105 -0
  139. package/src/mcp/effective-config.ts +42 -15
  140. package/src/mcp/manager.ts +61 -8
  141. package/src/mcp/mcp-auth-orchestrator.ts +17 -4
  142. package/src/mcp/mcp-auth-state.ts +38 -6
  143. package/src/mcp/mcp-oauth-provider.ts +101 -33
  144. package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
  145. package/src/monitoring/mount-watch.ts +348 -0
  146. package/src/monitoring/worker.ts +9 -0
  147. package/src/oauth/__tests__/seed-providers-managed.test.ts +95 -0
  148. package/src/oauth/connection-resolver.test.ts +27 -0
  149. package/src/oauth/connection-resolver.ts +25 -1
  150. package/src/oauth/provider-serializer.ts +9 -0
  151. package/src/oauth/seed-providers.ts +121 -2
  152. package/src/onboarding/checkin-event.ts +2 -1
  153. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
  154. package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
  155. package/src/plugins/external-plugin-loader.ts +49 -86
  156. package/src/plugins/installed-plugin-dirs.ts +6 -4
  157. package/src/plugins/mcp-servers.ts +9 -15
  158. package/src/plugins/mtime-cache.ts +2 -1
  159. package/src/plugins/user-loader.ts +2 -1
  160. package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
  161. package/src/providers/inference/endpoint-probe.ts +27 -12
  162. package/src/providers/openai/responses-provider.ts +9 -2
  163. package/src/providers/opencode/client.test.ts +167 -0
  164. package/src/providers/opencode/client.ts +73 -4
  165. package/src/runtime/AGENTS.md +4 -0
  166. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
  167. package/src/runtime/http-server.ts +6 -6
  168. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  169. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
  170. package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
  171. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  172. package/src/runtime/routes/app-management-routes.ts +17 -2
  173. package/src/runtime/routes/browser-context.ts +39 -0
  174. package/src/runtime/routes/browser-routes.ts +16 -47
  175. package/src/runtime/routes/browser-tabs-routes.ts +72 -29
  176. package/src/runtime/routes/credential-routes.ts +1 -4
  177. package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
  178. package/src/runtime/routes/desktop-setup-routes.ts +7 -5
  179. package/src/runtime/routes/mcp-auth-routes.ts +106 -94
  180. package/src/runtime/routes/oauth-commands-routes.ts +14 -0
  181. package/src/runtime/routes/oauth-providers.ts +7 -0
  182. package/src/runtime/routes/plugins-routes.ts +14 -1
  183. package/src/runtime/sync/resource-sync-events.ts +4 -0
  184. package/src/tools/browser/browser-execution.ts +44 -13
  185. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
  186. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
  187. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
  188. package/src/tools/computer-use/definitions.ts +3 -4
  189. package/src/tools/ui-surface/channel-variants.ts +10 -59
  190. package/src/util/abort-reasons.ts +12 -1
  191. package/src/util/plugin-manifest.ts +203 -0
  192. package/src/watch/watch-retro.ts +6 -8
  193. package/src/desktop/desktop-feature.test.ts +0 -18
  194. package/src/desktop/desktop-feature.ts +0 -19
@@ -1490,6 +1490,7 @@ describe("classifyConversationError", () => {
1490
1490
  "subagent_aborted",
1491
1491
  "signal_cancel",
1492
1492
  "voice_session_aborted",
1493
+ "voice_progress_narration_timeout",
1493
1494
  ];
1494
1495
 
1495
1496
  for (const kind of taggedKinds) {
@@ -1727,7 +1728,9 @@ describe("ConnectionResolutionError classification", () => {
1727
1728
  expect(result.userMessage).toContain("qwen/qwen3-8b");
1728
1729
  expect(result.userMessage).toContain("Vellum GPU route");
1729
1730
  expect(result.userMessage).toContain('profile "steer"');
1730
- expect(result.userMessage).toContain("was not sent through another provider");
1731
+ expect(result.userMessage).toContain(
1732
+ "was not sent through another provider",
1733
+ );
1731
1734
  });
1732
1735
 
1733
1736
  it("classifies missing_credential naming the connection and fix", () => {
@@ -119,6 +119,7 @@ import {
119
119
  applyRuntimeInjections,
120
120
  assembleSlackActiveThreadFocusBlock,
121
121
  assembleSlackChronologicalMessages,
122
+ buildChannelCapabilityBlock,
122
123
  buildSubagentStatusBlock,
123
124
  getSlackCompactionWatermarkForPrefix,
124
125
  getSlackWatermarkAdvanceForRowPrefix,
@@ -586,6 +587,35 @@ describe("injectChannelCapabilityContext", () => {
586
587
  expect(text).not.toContain("Do NOT use ui_show, ui_update, or app_create");
587
588
  });
588
589
 
590
+ test("guides non-interactive turns to persist UI for a later capable client", () => {
591
+ const slack: ChannelCapabilities = {
592
+ channel: "slack",
593
+ dashboardCapable: false,
594
+ supportsDynamicUi: false,
595
+ supportsVoiceInput: false,
596
+ };
597
+ const phone: ChannelCapabilities = {
598
+ channel: "phone",
599
+ dashboardCapable: false,
600
+ supportsDynamicUi: false,
601
+ supportsVoiceInput: false,
602
+ };
603
+
604
+ for (const caps of [slack, phone]) {
605
+ const text = buildChannelCapabilityBlock(caps, undefined, true)!;
606
+ expect(text).toContain(
607
+ "ui_show, ui_update, and ui_dismiss persist conversation content",
608
+ );
609
+ expect(text).not.toContain("Only use ui_show/ui_update");
610
+ expect(text).not.toContain(
611
+ "Do NOT use ui_show, ui_update, or app_create",
612
+ );
613
+ expect(text).not.toContain(
614
+ "Present information as well-formatted text instead of dynamic UI.",
615
+ );
616
+ }
617
+ });
618
+
589
619
  test("keeps blanket ui_show/ui_update prohibition for other non-dynamic channels", () => {
590
620
  const caps: ChannelCapabilities = {
591
621
  channel: "phone",
@@ -870,12 +900,12 @@ describe("trust-gating via channel capabilities", () => {
870
900
 
871
901
  const result = injectChannelCapabilityContext(message, caps);
872
902
 
873
- // macOS clients now get osascript guidance injected
903
+ // macOS clients get app-scripting guidance injected
874
904
  expect(result).not.toBe(message);
875
905
  const injected = (result.content[0] as { type: "text"; text: string }).text;
876
906
  expect(injected).toContain("client_os: macos");
877
- expect(injected).toContain("osascript");
878
- expect(injected).toContain("host_bash");
907
+ expect(injected).toContain("drive apps with the computer-use skill");
908
+ expect(injected).toContain("`host_bash` is for shell commands");
879
909
  // No channel constraints — full desktop capabilities
880
910
  expect(injected).not.toContain("CHANNEL CONSTRAINTS");
881
911
  });
@@ -24,6 +24,7 @@ function proxyDouble() {
24
24
  recordAction,
25
25
  request,
26
26
  reset: () => {},
27
+ endTask: () => {},
27
28
  stepCount: 0,
28
29
  },
29
30
  };
@@ -30,6 +30,7 @@ function proxyDouble() {
30
30
  recordAction: () => {},
31
31
  request,
32
32
  reset: () => {},
33
+ endTask: () => {},
33
34
  stepCount: 0,
34
35
  },
35
36
  };
@@ -532,6 +532,28 @@ describe("credentials routes", () => {
532
532
  );
533
533
  });
534
534
 
535
+ test("stores a pasted Claude token without touching leftover refresh material", async () => {
536
+ secureStore.set("acp:claude_oauth_refresh_token", "stale-refresh");
537
+ secureStore.set("acp:claude_oauth_expires_at", "111");
538
+
539
+ await setRoute!.handler({
540
+ body: {
541
+ service: "acp",
542
+ field: "claude_oauth_token",
543
+ value: "sk-ant-oat01-pasted-token",
544
+ },
545
+ });
546
+
547
+ expect(secureStore.get("acp:claude_oauth_token")).toBe(
548
+ "sk-ant-oat01-pasted-token",
549
+ );
550
+ expect(secureStore.get("acp:claude_oauth_refresh_token")).toBe(
551
+ "stale-refresh",
552
+ );
553
+ expect(secureStore.get("acp:claude_oauth_expires_at")).toBe("111");
554
+ });
555
+
556
+
535
557
  test("a successful set scrubs the normalized value from transcripts exactly once", async () => {
536
558
  /**
537
559
  * The pasted plaintext may already sit in recent transcripts, so a
@@ -1301,5 +1323,21 @@ describe("credentials routes", () => {
1301
1323
  // THEN it rejects with a BadRequestError
1302
1324
  await expect(call).rejects.toBeInstanceOf(BadRequestError);
1303
1325
  });
1326
+
1327
+ test("deletes only the requested Claude access-token field", async () => {
1328
+ secureStore.set("acp:claude_oauth_token", "sk-ant-oat01-connected");
1329
+ secureStore.set("acp:claude_oauth_refresh_token", "refresh-leftover");
1330
+ secureStore.set("acp:claude_oauth_expires_at", "111");
1331
+
1332
+ await deleteRoute!.handler({
1333
+ body: { service: "acp", field: "claude_oauth_token" },
1334
+ });
1335
+
1336
+ expect(secureStore.has("acp:claude_oauth_token")).toBe(false);
1337
+ expect(secureStore.get("acp:claude_oauth_refresh_token")).toBe(
1338
+ "refresh-leftover",
1339
+ );
1340
+ expect(secureStore.get("acp:claude_oauth_expires_at")).toBe("111");
1341
+ });
1304
1342
  });
1305
1343
  });
@@ -190,7 +190,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
190
190
  "oauth/credential-token-resolver.ts", // centralized access-token key resolution for OAuth and manual-token providers
191
191
  "oauth/connection-resolver.ts", // resolve OAuthConnection from oauth-store (access_token lookup)
192
192
  "runtime/routes/secret-routes.ts", // HTTP secret management routes (set/delete secrets)
193
- "acp/acp-claude-oauth.ts", // Connect Claude OAuth token vault-store helper (stores sk-ant-oat token via setSecureKeyAsync)
193
+ "acp/acp-claude-oauth.ts", // Connect Claude OAuth token vault-store helper (stores access token plus broker-owned refresh/expiry companions via setSecureKeyAsync)
194
194
  "runtime/routes/acp-claude-auth-routes.ts", // Connect Claude Code daemon OAuth flow (stores OAuth token in vault)
195
195
  "runtime/routes/migration-routes.ts", // migration import credential restore
196
196
  "daemon/conversation-messaging.ts", // credential storage during session messaging
@@ -485,8 +485,12 @@ describe("surfaceProxyResolver — CU tool routing", () => {
485
485
  // Proxy state is clean after done
486
486
  expect(proxy.stepCount).toBe(0);
487
487
  expect(proxy.actionHistory).toHaveLength(0);
488
- // Only 2 messages sent to client (screenshot + click; done is terminal)
489
- expect(sentMessages).toHaveLength(2);
488
+ // screenshot + click, then done tells the client the task ended so the
489
+ // helper returns the pointer; done itself never reaches the client
490
+ expect(sentMessages).toHaveLength(3);
491
+ expect((sentMessages[2] as Record<string, unknown>).type).toBe(
492
+ "host_cu_cancel",
493
+ );
490
494
  });
491
495
  });
492
496
 
@@ -39,6 +39,20 @@ function writePackageJson(dir: string, pkg: Record<string, unknown>): void {
39
39
  writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2));
40
40
  }
41
41
 
42
+ function writePluginJson(dir: string, manifest: Record<string, unknown>): void {
43
+ writeFileSync(
44
+ join(dir, "plugin.json"),
45
+ JSON.stringify(
46
+ {
47
+ $schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
48
+ ...manifest,
49
+ },
50
+ null,
51
+ 2,
52
+ ),
53
+ );
54
+ }
55
+
42
56
  function writeSurfaceFile(dir: string, relPath: string, body: string): void {
43
57
  const parts = relPath.split("/");
44
58
  parts.pop();
@@ -104,6 +118,31 @@ describe("loadExternalPlugin — manifest", () => {
104
118
  expect(registered).toBeDefined();
105
119
  expect(registered?.manifest.version).toBe("0.0.0");
106
120
  });
121
+
122
+ test("loads a standard plugin.json when package.json is absent", async () => {
123
+ const dir = freshPluginDir("standard-only");
124
+ writePluginJson(dir, { name: "standard-only", version: "2.1.0" });
125
+
126
+ await loadExternalPlugin(dir);
127
+
128
+ const registered = getRegisteredPlugins().find(
129
+ (plugin) => plugin.manifest.name === "standard-only",
130
+ );
131
+ expect(registered?.manifest.version).toBe("2.1.0");
132
+ });
133
+
134
+ test("keeps package.json authoritative when plugin.json is also present", async () => {
135
+ const dir = freshPluginDir("mixed-manifests");
136
+ writePackageJson(dir, { name: "legacy-name", version: "1.2.3" });
137
+ writeFileSync(join(dir, "plugin.json"), "{ foreign and malformed }");
138
+
139
+ await loadExternalPlugin(dir);
140
+
141
+ const registered = getRegisteredPlugins().find(
142
+ (plugin) => plugin.manifest.name === "mixed-manifests",
143
+ );
144
+ expect(registered?.manifest.version).toBe("1.2.3");
145
+ });
107
146
  });
108
147
 
109
148
  describe("credentialKeyPatterns manifest field", () => {
@@ -277,6 +316,19 @@ describe("loadExternalPlugin — plugin-api peerDependency", () => {
277
316
  test("malformed package.json is logged and skipped (registry untouched)", async () => {
278
317
  const dir = freshPluginDir("malformed-pkg");
279
318
  writeFileSync(join(dir, "package.json"), "{ this is not json");
319
+ writePluginJson(dir, { name: "valid-standard-fallback" });
320
+
321
+ await loadExternalPlugin(dir);
322
+
323
+ expect(registeredNames()).toHaveLength(0);
324
+ });
325
+
326
+ test("malformed selected plugin.json is logged and skipped", async () => {
327
+ const dir = freshPluginDir("malformed-standard");
328
+ writeFileSync(
329
+ join(dir, "plugin.json"),
330
+ JSON.stringify({ name: "missing-schema" }),
331
+ );
280
332
 
281
333
  await loadExternalPlugin(dir);
282
334
 
@@ -41,6 +41,16 @@ function makeFakeCdp(
41
41
  dispose() {
42
42
  _cdpDisposed = true;
43
43
  },
44
+ setCdpSessionId() {},
45
+ async listTabs() {
46
+ return [];
47
+ },
48
+ async selectTab(tabId: number) {
49
+ return { tabId };
50
+ },
51
+ async closeTab(tabId: number) {
52
+ return { tabId, closed: true };
53
+ },
44
54
  };
45
55
  }
46
56
 
@@ -142,6 +152,8 @@ mock.module("../tools/network/url-safety.js", () => ({
142
152
 
143
153
  import {
144
154
  executeBrowserClick,
155
+ executeBrowserClose,
156
+ executeBrowserDetach,
145
157
  executeBrowserNavigate,
146
158
  executeBrowserScreenshot,
147
159
  executeBrowserSnapshot,
@@ -687,6 +699,42 @@ describe("browser_mode wiring through tool execution", () => {
687
699
 
688
700
  // ── Per-conversation sticky backend kind ─────────────────────────
689
701
 
702
+ for (const release of [executeBrowserDetach, executeBrowserClose]) {
703
+ test.each(["local", "extension", "cdp-inspect"])(
704
+ `${release.name} on desktop preserves the normal %s session's backend`,
705
+ async (mode) => {
706
+ const first = await executeBrowserSnapshot({ browser_mode: mode }, ctx);
707
+ expect(first.isError).toBe(false);
708
+
709
+ factoryModeCalls.length = 0;
710
+ const desktop = await release(
711
+ {},
712
+ {
713
+ ...ctx,
714
+ cdpClient: makeFakeCdp(
715
+ "cdp-inspect",
716
+ `desktop-browser:${ctx.conversationId}`,
717
+ ),
718
+ },
719
+ );
720
+ expect(desktop.isError).toBe(false);
721
+ expect(factoryModeCalls).toEqual([]);
722
+
723
+ const resumed = await executeBrowserSnapshot({}, ctx);
724
+ expect(resumed.isError).toBe(false);
725
+ expect(factoryModeCalls).toEqual([mode]);
726
+ },
727
+ );
728
+
729
+ test(`${release.name} on the normal browser still resets automatic selection`, async () => {
730
+ await executeBrowserSnapshot({ browser_mode: "extension" }, ctx);
731
+ expect((await release({}, ctx)).isError).toBe(false);
732
+ factoryModeCalls.length = 0;
733
+ expect((await executeBrowserSnapshot({}, ctx)).isError).toBe(false);
734
+ expect(factoryModeCalls).toEqual(["auto"]);
735
+ });
736
+ }
737
+
690
738
  test("auto-mode call after an explicit pin sticks to the pinned kind", async () => {
691
739
  const first = await executeBrowserNavigate(
692
740
  { url: "https://example.com", browser_mode: "local" },