@vellumai/assistant 0.11.10 → 0.11.11-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +1 -1
  2. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  3. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  6. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  12. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  13. package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  14. package/openapi.yaml +152 -55
  15. package/package.json +1 -1
  16. package/src/__tests__/anthropic-provider.test.ts +60 -0
  17. package/src/__tests__/config-get-vision-flag.test.ts +36 -0
  18. package/src/__tests__/conversation-error.test.ts +38 -0
  19. package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
  20. package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
  21. package/src/__tests__/headless-browser-mode.test.ts +63 -13
  22. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  23. package/src/__tests__/heartbeat-service.test.ts +118 -27
  24. package/src/__tests__/host-app-control-proxy.test.ts +2 -2
  25. package/src/__tests__/host-app-control-routes.test.ts +41 -0
  26. package/src/__tests__/host-bash-routes.test.ts +30 -1
  27. package/src/__tests__/host-browser-routes.test.ts +34 -0
  28. package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
  29. package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
  30. package/src/__tests__/host-proxy-base.test.ts +1 -0
  31. package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
  32. package/src/__tests__/llm-schema.test.ts +32 -0
  33. package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
  34. package/src/__tests__/memory-config-file-parity.test.ts +18 -0
  35. package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
  36. package/src/__tests__/messaging-send-tool.test.ts +159 -13
  37. package/src/__tests__/messaging-shared.test.ts +19 -0
  38. package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
  39. package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
  40. package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
  41. package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
  42. package/src/__tests__/schedule-routes.test.ts +30 -1
  43. package/src/__tests__/scheduler-result-notification.test.ts +209 -0
  44. package/src/__tests__/skills-install-extract.test.ts +26 -0
  45. package/src/__tests__/skills-install-staging.test.ts +11 -2
  46. package/src/api/constants/personality-sliders.test.ts +21 -0
  47. package/src/api/constants/personality-sliders.ts +64 -0
  48. package/src/api/index.ts +8 -0
  49. package/src/archive/ustar-size.test.ts +32 -0
  50. package/src/archive/ustar-size.ts +23 -0
  51. package/src/background-wake/next-wake.test.ts +13 -0
  52. package/src/background-wake/next-wake.ts +4 -2
  53. package/src/calls/voice-session-bridge.ts +26 -0
  54. package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
  55. package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
  56. package/src/cli/commands/config.help.ts +3 -1
  57. package/src/cli/commands/memory/worker.ts +1 -1
  58. package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
  59. package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
  60. package/src/cli/commands/platform/credits-format.ts +57 -0
  61. package/src/cli/commands/platform/index.help.ts +19 -3
  62. package/src/cli/commands/plugins.help.ts +4 -1
  63. package/src/cli/commands/plugins.ts +2 -1
  64. package/src/cli/lib/bundled-marketplace.json +1 -1
  65. package/src/cli/lib/install-from-platform.ts +8 -10
  66. package/src/config/bundled-skills/messaging/SKILL.md +2 -0
  67. package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
  68. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
  69. package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
  70. package/src/config/bundled-skills/schedule/SKILL.md +9 -1
  71. package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
  72. package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
  73. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
  74. package/src/config/feature-flag-registry.json +17 -1
  75. package/src/config/input-modalities.test.ts +71 -0
  76. package/src/config/input-modalities.ts +95 -0
  77. package/src/config/schemas/heartbeat.ts +3 -3
  78. package/src/config/schemas/llm.ts +13 -3
  79. package/src/config/schemas/memory-storage.test.ts +39 -0
  80. package/src/config/schemas/memory-storage.ts +21 -0
  81. package/src/context/token-estimator.ts +2 -2
  82. package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
  83. package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
  84. package/src/daemon/conversation-error.ts +20 -7
  85. package/src/daemon/embedding-reconcile.ts +64 -1
  86. package/src/daemon/handlers/config-embeddings.ts +31 -0
  87. package/src/daemon/host-app-control-proxy.ts +2 -0
  88. package/src/daemon/host-bash-proxy.ts +1 -0
  89. package/src/daemon/host-cu-proxy.ts +6 -6
  90. package/src/daemon/host-file-proxy.ts +6 -6
  91. package/src/daemon/host-proxy-base.ts +10 -9
  92. package/src/daemon/host-transfer-proxy.ts +21 -24
  93. package/src/daemon/mcp-reload-service.ts +6 -0
  94. package/src/daemon/providers-setup.ts +5 -26
  95. package/src/heartbeat/heartbeat-service.ts +72 -13
  96. package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
  97. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
  98. package/src/live-voice/live-voice-photo.ts +68 -4
  99. package/src/live-voice/live-voice-session.ts +36 -2
  100. package/src/live-voice/protocol.ts +98 -1
  101. package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
  102. package/src/mcp/__tests__/reload-signal.test.ts +57 -0
  103. package/src/mcp/__tests__/startup.test.ts +150 -0
  104. package/src/mcp/reload-signal.ts +50 -0
  105. package/src/mcp/startup.ts +101 -0
  106. package/src/messaging/providers/outlook/adapter.ts +2 -13
  107. package/src/messaging/providers/outlook/client.ts +13 -1
  108. package/src/messaging/providers/outlook/types.ts +3 -0
  109. package/src/notifications/__tests__/decision-engine.test.ts +79 -6
  110. package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
  111. package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
  112. package/src/notifications/copy-composer.ts +33 -11
  113. package/src/notifications/decision-engine.ts +48 -11
  114. package/src/notifications/events-store.ts +68 -2
  115. package/src/notifications/home-feed-side-effect.ts +15 -3
  116. package/src/notifications/schedule-result-producer.ts +275 -0
  117. package/src/notifications/signal.ts +5 -0
  118. package/src/oauth/seed-providers.ts +3 -2
  119. package/src/persistence/conversation-crud.ts +38 -0
  120. package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
  121. package/src/persistence/embeddings/embedding-backend.ts +148 -11
  122. package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
  123. package/src/persistence/embeddings/embedding-openai.ts +55 -4
  124. package/src/persistence/embeddings/embedding-types.test.ts +24 -0
  125. package/src/persistence/embeddings/embedding-types.ts +25 -1
  126. package/src/persistence/job-utils.ts +6 -2
  127. package/src/plugin-api/index.ts +2 -1
  128. package/src/plugin-api/vision-support.test.ts +81 -0
  129. package/src/plugin-api/vision-support.ts +57 -10
  130. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
  131. package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
  132. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
  133. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
  134. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
  135. package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
  136. package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
  137. package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
  138. package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
  139. package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
  140. package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
  141. package/src/providers/anthropic/client.ts +9 -4
  142. package/src/providers/inline-audio-support.test.ts +34 -0
  143. package/src/providers/inline-audio-support.ts +27 -0
  144. package/src/providers/model-catalog.ts +22 -2
  145. package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
  146. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
  147. package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
  148. package/src/providers/openai/api-error-normalization.ts +18 -3
  149. package/src/providers/openai/chat-completions-provider.ts +113 -13
  150. package/src/providers/openai/responses-provider.ts +10 -8
  151. package/src/providers/provider-secret-catalog.ts +11 -1
  152. package/src/providers/vellum/client.ts +14 -1
  153. package/src/providers/vellum/personality-directions.test.ts +163 -0
  154. package/src/providers/vellum/personality-directions.ts +120 -0
  155. package/src/providers/xml-tool-call-salvage.ts +212 -0
  156. package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
  157. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
  158. package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
  159. package/src/runtime/auth/same-actor.ts +29 -5
  160. package/src/runtime/background-job-runner.ts +38 -29
  161. package/src/runtime/guardian-action-service.ts +20 -1
  162. package/src/runtime/migrations/vbundle-validator.ts +3 -3
  163. package/src/runtime/pending-interactions.ts +4 -5
  164. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
  165. package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
  166. package/src/runtime/routes/conversation-query-routes.ts +53 -11
  167. package/src/runtime/routes/document-comments-routes.ts +8 -2
  168. package/src/runtime/routes/guardian-action-routes.ts +7 -0
  169. package/src/runtime/routes/heartbeat-routes.ts +41 -53
  170. package/src/runtime/routes/host-app-control-routes.ts +10 -36
  171. package/src/runtime/routes/host-bash-routes.ts +11 -45
  172. package/src/runtime/routes/host-browser-routes.ts +24 -0
  173. package/src/runtime/routes/host-cu-routes.ts +11 -45
  174. package/src/runtime/routes/host-file-routes.ts +11 -44
  175. package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
  176. package/src/runtime/routes/host-transfer-routes.ts +33 -25
  177. package/src/runtime/routes/index.ts +0 -2
  178. package/src/runtime/routes/integrations/a2a.ts +12 -3
  179. package/src/runtime/routes/integrations/vercel.ts +13 -3
  180. package/src/runtime/routes/platform-routes.ts +121 -24
  181. package/src/runtime/routes/schedule-routes.ts +15 -1
  182. package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
  183. package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
  184. package/src/schedule/schedule-timezone.ts +25 -0
  185. package/src/schedule/scheduler.ts +19 -0
  186. package/src/schedule/worker.ts +119 -2
  187. package/src/skills/catalog-install.ts +40 -5
  188. package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
  189. package/src/tools/browser/browser-execution.ts +117 -53
  190. package/src/tools/browser/browser-status-constants.ts +6 -6
  191. package/src/tools/capability-offer.test.ts +87 -0
  192. package/src/tools/capability-offer.ts +93 -0
  193. package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
  194. package/src/tools/terminal/safe-env.ts +3 -2
  195. package/src/util/provider-error-patterns.ts +34 -0
  196. package/src/watcher/__tests__/engine.test.ts +5 -2
  197. package/src/watcher/engine.ts +7 -4
  198. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
  199. package/src/runtime/routes/sight-frame-routes.ts +0 -136
@@ -14,6 +14,10 @@ import { homedir } from "node:os";
14
14
  import { dirname, join, posix, resolve, sep } from "node:path";
15
15
  import { gunzipSync } from "node:zlib";
16
16
 
17
+ import {
18
+ MALFORMED_USTAR_SIZE,
19
+ parseUstarSizeField,
20
+ } from "../archive/ustar-size.js";
17
21
  import { getPlatformBaseUrl } from "../config/env.js";
18
22
  import { loadSkillCatalog } from "../config/skills.js";
19
23
  import { isBunVirtualPath } from "../util/bundled-asset.js";
@@ -29,6 +33,35 @@ import {
29
33
 
30
34
  const log = getLogger("catalog-install");
31
35
 
36
+ /**
37
+ * `bun install` argv for a skill's declared dependencies. Lifecycle scripts
38
+ * are suppressed: skill archives (catalog tarballs, skills.sh, ClawHub) are
39
+ * unsigned, so a `preinstall`/`postinstall` hook would be arbitrary code
40
+ * execution at install time. Mirrors plugin install in
41
+ * `cli/lib/install-plugin-dependencies.ts`.
42
+ */
43
+ export const SKILL_DEPENDENCY_INSTALL_ARGS: readonly string[] = Object.freeze([
44
+ "install",
45
+ "--omit=dev",
46
+ "--ignore-scripts",
47
+ "--no-save",
48
+ ]);
49
+
50
+ export class SkillArchiveError extends Error {
51
+ constructor(message: string) {
52
+ super(message);
53
+ this.name = "SkillArchiveError";
54
+ }
55
+ }
56
+
57
+ function parseTarSize(header: Buffer): number {
58
+ try {
59
+ return parseUstarSizeField(header);
60
+ } catch {
61
+ throw new SkillArchiveError(MALFORMED_USTAR_SIZE);
62
+ }
63
+ }
64
+
32
65
  // ─── Types ───────────────────────────────────────────────────────────────────
33
66
 
34
67
  export interface CatalogSkill {
@@ -302,11 +335,13 @@ export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
302
335
  // File type (byte 156): '5' = directory, '0' or '\0' = regular file
303
336
  const typeFlag = header[156];
304
337
 
305
- // File size (bytes 124-135, octal)
306
- const sizeStr = header.subarray(124, 136).toString("utf-8").trim();
307
- const size = parseInt(sizeStr, 8) || 0;
338
+ const size = parseTarSize(header);
308
339
 
309
340
  offset += 512; // past header
341
+ const paddedSize = Math.ceil(size / 512) * 512;
342
+ if (offset + paddedSize > tarBuffer.length) {
343
+ throw new SkillArchiveError("tar entry size exceeds archive length");
344
+ }
310
345
 
311
346
  // Skip directories and empty names
312
347
  if (name && typeFlag !== 53 /* '5' */) {
@@ -325,7 +360,7 @@ export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
325
360
  }
326
361
 
327
362
  // Skip to next header (data padded to 512 bytes)
328
- offset += Math.ceil(size / 512) * 512;
363
+ offset += paddedSize;
329
364
  }
330
365
  return foundSkillMd;
331
366
  }
@@ -419,7 +454,7 @@ export async function installSkillDependenciesIfPresent(
419
454
  const env = { ...process.env };
420
455
  addToPathEnv(env, [join(homedir(), ".bun", "bin")]);
421
456
  await new Promise<void>((resolve, reject) => {
422
- const child = spawn("bun", ["install"], {
457
+ const child = spawn("bun", [...SKILL_DEPENDENCY_INSTALL_ARGS], {
423
458
  cwd: skillDir,
424
459
  stdio: "inherit",
425
460
  env,
@@ -6,6 +6,7 @@ import {
6
6
  BROWSER_STATUS_INPUT_FIELD,
7
7
  BROWSER_STATUS_MODE,
8
8
  CHROME_EXTENSION_INSTALL_HINT,
9
+ DESKTOP_APP_INSTALL_HINT,
9
10
  } from "../browser-status-constants.js";
10
11
  import { CdpError } from "../cdp-client/errors.js";
11
12
 
@@ -217,6 +218,7 @@ describe("executeBrowserStatus", () => {
217
218
  expect(extension.details.transport).toBe("extension-ws");
218
219
  expect(extension.userActions).toEqual([
219
220
  CHROME_EXTENSION_INSTALL_HINT,
221
+ DESKTOP_APP_INSTALL_HINT,
220
222
  "Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
221
223
  ]);
222
224
  });
@@ -244,6 +246,7 @@ describe("executeBrowserStatus", () => {
244
246
  expect(extension.summary).toContain("probe failed");
245
247
  expect(extension.userActions).toEqual([
246
248
  CHROME_EXTENSION_INSTALL_HINT,
249
+ DESKTOP_APP_INSTALL_HINT,
247
250
  "Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
248
251
  ]);
249
252
  });
@@ -6,6 +6,11 @@ import { wrapUntrustedContent } from "../../security/untrusted-content.js";
6
6
  import { getLogger } from "../../util/logger.js";
7
7
  import { truncate } from "../../util/truncate.js";
8
8
  import { safeStringSlice } from "../../util/unicode.js";
9
+ import {
10
+ appendLoggedInBrowserOffer,
11
+ formatLoggedInBrowserOffer,
12
+ shouldOfferLoggedInBrowser,
13
+ } from "../capability-offer.js";
9
14
  import { credentialBroker } from "../credentials/broker.js";
10
15
  import { BROWSER_FILL_CAPABILITY } from "../credentials/tool-policy.js";
11
16
  import {
@@ -40,7 +45,7 @@ import {
40
45
  type BrowserStatusMode,
41
46
  CDP_INSPECT_STATUS_DISCOVERY_CODE,
42
47
  CHROME_EXTENSION_INSTALL_HINT,
43
- CHROME_WEB_STORE_INSTALL_URL,
48
+ DESKTOP_APP_INSTALL_HINT,
44
49
  } from "./browser-status-constants.js";
45
50
  import {
46
51
  formatAxSnapshot,
@@ -298,10 +303,9 @@ const REMEDIATION_HINTS: Record<string, string[]> = {
298
303
  // Extension backend
299
304
  "extension:transport_error": [
300
305
  CHROME_EXTENSION_INSTALL_HINT,
301
- "Ensure the Vellum browser extension is installed and enabled, or that the macOS desktop client is running for host browser proxy mode.",
302
- "For extension mode: check that the extension WebSocket connection is active (extension popup → status).",
303
- "For macOS host browser proxy: verify the desktop client is running and has an active SSE connection to the assistant.",
304
- "Try reconnecting the extension or restarting the desktop client.",
306
+ DESKTOP_APP_INSTALL_HINT,
307
+ "Make sure Chrome is open with the Vellum extension enabled, or that the desktop app is running.",
308
+ "In the extension popup, check that the status shows connected.",
305
309
  ],
306
310
  // cdp-inspect backend — discovery-level failures
307
311
  "cdp-inspect:unreachable": [
@@ -334,15 +338,16 @@ const REMEDIATION_HINTS: Record<string, string[]> = {
334
338
  ],
335
339
  // Host-bridge backend (desktop SSE bridge → user's Chrome debug port)
336
340
  "host-bridge:unreachable": [
337
- "Ensure Chrome on the user's machine is on version 146 or higher (chrome://settings/help).",
338
- 'Ensure "Allow remote debugging for this browser instance" is toggled on at chrome://inspect/#remote-debugging.',
339
- "Verify the desktop client is running and has an active SSE connection to the assistant.",
340
- `Installing the Vellum Chrome extension is the preferred path and avoids the debug-port requirement: ${CHROME_WEB_STORE_INSTALL_URL}`,
341
+ CHROME_EXTENSION_INSTALL_HINT,
342
+ DESKTOP_APP_INSTALL_HINT,
343
+ "The Chrome extension is the preferred path and avoids the debug-port requirement.",
344
+ "If using remote debugging instead, Chrome 146+ must have remote debugging enabled at chrome://inspect/#remote-debugging.",
341
345
  ],
342
346
  "host-bridge:transport_error": [
343
- "The desktop client could not reach Chrome's remote-debugging endpoint on the user's machine.",
344
- `Ensure Chrome is running with remote debugging enabled, or install the Vellum Chrome extension (preferred): ${CHROME_WEB_STORE_INSTALL_URL}`,
345
- "Verify the desktop client is running and connected.",
347
+ CHROME_EXTENSION_INSTALL_HINT,
348
+ DESKTOP_APP_INSTALL_HINT,
349
+ "The desktop app could not reach Chrome's remote-debugging endpoint.",
350
+ "Install the Chrome extension, or enable remote debugging and confirm the desktop app is running.",
346
351
  ],
347
352
  // Local/Playwright backend
348
353
  "local:transport_error": [
@@ -357,13 +362,15 @@ const REMEDIATION_HINTS: Record<string, string[]> = {
357
362
  * pinned-mode failure. Includes:
358
363
  * - the requested mode
359
364
  * - ordered attempted modes with exact failure reasons
360
- * - a remediation checklist tailored by backend and failure code
365
+ * - a logged-in-browser offer (desktop app and Chrome extension)
366
+ * - setup details tailored by backend and failure code
361
367
  *
362
368
  * Exported for testing.
363
369
  */
364
370
  export function formatModeSelectionFailure(
365
371
  requestedMode: BrowserMode,
366
372
  error: CdpError,
373
+ context?: Pick<ToolContext, "transportInterface" | "clientOs">,
367
374
  ): string {
368
375
  const lines: string[] = [];
369
376
  lines.push(`Error: Browser mode "${requestedMode}" failed.`);
@@ -388,10 +395,12 @@ export function formatModeSelectionFailure(
388
395
  lines.push("");
389
396
  }
390
397
 
391
- // Collect remediation hints
398
+ lines.push(formatLoggedInBrowserOffer(context));
399
+ lines.push("");
400
+
392
401
  const hints = collectRemediationHints(diagnostics, error);
393
402
  if (hints.length > 0) {
394
- lines.push("Remediation:");
403
+ lines.push("Setup details:");
395
404
  for (const hint of hints) {
396
405
  lines.push(` - ${hint}`);
397
406
  }
@@ -568,7 +577,7 @@ async function acquireCdpClientWithMode(
568
577
  if (retryErr instanceof CdpError) {
569
578
  return {
570
579
  errorResult: {
571
- content: formatModeSelectionFailure("auto", retryErr),
580
+ content: formatModeSelectionFailure("auto", retryErr, context),
572
581
  isError: true,
573
582
  },
574
583
  };
@@ -579,7 +588,7 @@ async function acquireCdpClientWithMode(
579
588
  if (err instanceof CdpError && browserMode !== "auto") {
580
589
  return {
581
590
  errorResult: {
582
- content: formatModeSelectionFailure(browserMode, err),
591
+ content: formatModeSelectionFailure(browserMode, err, context),
583
592
  isError: true,
584
593
  },
585
594
  };
@@ -658,6 +667,7 @@ function wrapWithKindMemo(
658
667
  function formatCdpSendDiagnostics(
659
668
  err: unknown,
660
669
  browserMode: BrowserMode,
670
+ context?: Pick<ToolContext, "transportInterface" | "clientOs">,
661
671
  ): string | null {
662
672
  if (
663
673
  err instanceof CdpError &&
@@ -665,11 +675,60 @@ function formatCdpSendDiagnostics(
665
675
  err.code === "transport_error" &&
666
676
  err.attemptDiagnostics
667
677
  ) {
668
- return formatModeSelectionFailure(browserMode, err);
678
+ return formatModeSelectionFailure(browserMode, err, context);
669
679
  }
670
680
  return null;
671
681
  }
672
682
 
683
+ /**
684
+ * Format a browser-tool catch as a tool-response error. Mode-selection
685
+ * failures already carry the logged-in-browser offer. Reachability and
686
+ * auth failures get the same offer appended so the model relays a product
687
+ * the user can turn on instead of asking for a screenshot.
688
+ */
689
+ function formatBrowserToolFailure(
690
+ operation: string,
691
+ err: unknown,
692
+ browserMode: BrowserMode,
693
+ context: Pick<ToolContext, "transportInterface" | "clientOs">,
694
+ ): string {
695
+ const diagnosticMessage = formatCdpSendDiagnostics(err, browserMode, context);
696
+ if (diagnosticMessage) {
697
+ return diagnosticMessage;
698
+ }
699
+ const msg = err instanceof Error ? err.message : String(err);
700
+ const base = `Error: ${operation} failed: ${msg}`;
701
+ if (shouldOfferLoggedInBrowser(msg)) {
702
+ return appendLoggedInBrowserOffer(base, context);
703
+ }
704
+ return base;
705
+ }
706
+
707
+ function appendLoginFormGuidance(
708
+ lines: string[],
709
+ context: ToolContext,
710
+ backendKind: string,
711
+ ): void {
712
+ lines.push("Handle this by interacting with the login form:");
713
+ lines.push("1. Take a snapshot to find the sign-in form elements");
714
+ lines.push(
715
+ "2. Use credential fill to enter email/password from the credential vault",
716
+ );
717
+ lines.push(
718
+ "3. For email verification codes, use ui_show with a form to request the code mid-turn",
719
+ );
720
+ lines.push(
721
+ "4. Do NOT give up or suggest manual sign-in - handle the login flow yourself",
722
+ );
723
+ if (backendKind === "local") {
724
+ lines.push("");
725
+ lines.push(
726
+ "If this page needs SSO, a company VPN, or a login a fresh browser cannot complete:",
727
+ );
728
+ lines.push(formatLoggedInBrowserOffer(context));
729
+ }
730
+ }
731
+
673
732
  // ── Shared element resolution ────────────────────────────────────────
674
733
 
675
734
  /**
@@ -1234,19 +1293,7 @@ export async function executeBrowserNavigate(
1234
1293
  ),
1235
1294
  );
1236
1295
  lines.push("");
1237
- lines.push("Handle this by interacting with the login form:");
1238
- lines.push(
1239
- "1. Take a snapshot to find the sign-in form elements",
1240
- );
1241
- lines.push(
1242
- "2. Use credential fill to enter email/password from the credential vault",
1243
- );
1244
- lines.push(
1245
- "3. For email verification codes, use ui_show with a form to request the code mid-turn",
1246
- );
1247
- lines.push(
1248
- "4. Do NOT give up or suggest manual sign-in - handle the login flow yourself",
1249
- );
1296
+ appendLoginFormGuidance(lines, context, cdp.kind);
1250
1297
  }
1251
1298
  } else {
1252
1299
  lines.push("");
@@ -1256,6 +1303,10 @@ export async function executeBrowserNavigate(
1256
1303
  lines.push(
1257
1304
  "This challenge requires human verification. Surface this clearly: the page cannot be accessed until the verification is solved manually.",
1258
1305
  );
1306
+ if (cdp.kind === "local") {
1307
+ lines.push("");
1308
+ lines.push(formatLoggedInBrowserOffer(context));
1309
+ }
1259
1310
  }
1260
1311
  } else {
1261
1312
  // Login / 2FA / OAuth - the agent should handle these itself
@@ -1271,17 +1322,7 @@ export async function executeBrowserNavigate(
1271
1322
  ),
1272
1323
  );
1273
1324
  lines.push("");
1274
- lines.push("Handle this by interacting with the login form:");
1275
- lines.push("1. Take a snapshot to find the sign-in form elements");
1276
- lines.push(
1277
- "2. Use credential fill to enter email/password from the credential vault",
1278
- );
1279
- lines.push(
1280
- "3. For email verification codes, use ui_show with a form to request the code mid-turn",
1281
- );
1282
- lines.push(
1283
- "4. Do NOT give up or suggest manual sign-in - handle the login flow yourself",
1284
- );
1325
+ appendLoginFormGuidance(lines, context, cdp.kind);
1285
1326
  }
1286
1327
  }
1287
1328
  } catch {
@@ -1312,14 +1353,16 @@ export async function executeBrowserNavigate(
1312
1353
  };
1313
1354
  }
1314
1355
 
1315
- const diagnosticMessage = formatCdpSendDiagnostics(err, browserMode);
1316
- if (diagnosticMessage) {
1317
- return { content: diagnosticMessage, isError: true };
1318
- }
1319
-
1320
- const msg = err instanceof Error ? err.message : String(err);
1321
1356
  log.error({ err, url: safeRequestedUrl }, "Navigation failed");
1322
- return { content: `Error: Navigation failed: ${msg}`, isError: true };
1357
+ return {
1358
+ content: formatBrowserToolFailure(
1359
+ "Navigation",
1360
+ err,
1361
+ browserMode,
1362
+ context,
1363
+ ),
1364
+ isError: true,
1365
+ };
1323
1366
  } finally {
1324
1367
  cdp.dispose();
1325
1368
  }
@@ -1375,7 +1418,11 @@ export async function executeBrowserSnapshot(
1375
1418
  isError: false,
1376
1419
  };
1377
1420
  } catch (err) {
1378
- const diagnosticMessage = formatCdpSendDiagnostics(err, browserMode);
1421
+ const diagnosticMessage = formatCdpSendDiagnostics(
1422
+ err,
1423
+ browserMode,
1424
+ context,
1425
+ );
1379
1426
  if (diagnosticMessage) {
1380
1427
  return { content: diagnosticMessage, isError: true };
1381
1428
  }
@@ -1434,7 +1481,11 @@ export async function executeBrowserScreenshot(
1434
1481
  contentBlocks: [imageBlock],
1435
1482
  };
1436
1483
  } catch (err) {
1437
- const diagnosticMessage = formatCdpSendDiagnostics(err, browserMode);
1484
+ const diagnosticMessage = formatCdpSendDiagnostics(
1485
+ err,
1486
+ browserMode,
1487
+ context,
1488
+ );
1438
1489
  if (diagnosticMessage) {
1439
1490
  return { content: diagnosticMessage, isError: true };
1440
1491
  }
@@ -1489,6 +1540,7 @@ export async function executeBrowserAttach(
1489
1540
  const diagnosticMessage = formatCdpSendDiagnostics(
1490
1541
  err,
1491
1542
  acquired.browserMode,
1543
+ context,
1492
1544
  );
1493
1545
  if (diagnosticMessage) {
1494
1546
  return { content: diagnosticMessage, isError: true };
@@ -1535,6 +1587,7 @@ export async function executeBrowserDetach(
1535
1587
  const diagnosticMessage = formatCdpSendDiagnostics(
1536
1588
  err,
1537
1589
  acquired.browserMode,
1590
+ context,
1538
1591
  );
1539
1592
  if (diagnosticMessage) {
1540
1593
  return { content: diagnosticMessage, isError: true };
@@ -1611,6 +1664,7 @@ export async function executeBrowserClose(
1611
1664
  const diagnosticMessage = formatCdpSendDiagnostics(
1612
1665
  err,
1613
1666
  acquired.browserMode,
1667
+ context,
1614
1668
  );
1615
1669
  if (diagnosticMessage) {
1616
1670
  return { content: diagnosticMessage, isError: true };
@@ -1669,6 +1723,7 @@ export async function executeBrowserClick(
1669
1723
  const diagnosticMessage = formatCdpSendDiagnostics(
1670
1724
  err,
1671
1725
  acquired.browserMode,
1726
+ context,
1672
1727
  );
1673
1728
  if (diagnosticMessage) {
1674
1729
  return { content: diagnosticMessage, isError: true };
@@ -1799,6 +1854,7 @@ export async function executeBrowserType(
1799
1854
  const diagnosticMessage = formatCdpSendDiagnostics(
1800
1855
  err,
1801
1856
  acquired.browserMode,
1857
+ context,
1802
1858
  );
1803
1859
  if (diagnosticMessage) {
1804
1860
  return { content: diagnosticMessage, isError: true };
@@ -1874,6 +1930,7 @@ export async function executeBrowserPressKey(
1874
1930
  const diagnosticMessage = formatCdpSendDiagnostics(
1875
1931
  err,
1876
1932
  acquired.browserMode,
1933
+ context,
1877
1934
  );
1878
1935
  if (diagnosticMessage) {
1879
1936
  return { content: diagnosticMessage, isError: true };
@@ -1949,6 +2006,7 @@ export async function executeBrowserScroll(
1949
2006
  const diagnosticMessage = formatCdpSendDiagnostics(
1950
2007
  err,
1951
2008
  acquired.browserMode,
2009
+ context,
1952
2010
  );
1953
2011
  if (diagnosticMessage) {
1954
2012
  return { content: diagnosticMessage, isError: true };
@@ -2085,6 +2143,7 @@ export async function executeBrowserSelectOption(
2085
2143
  const diagnosticMessage = formatCdpSendDiagnostics(
2086
2144
  err,
2087
2145
  acquired.browserMode,
2146
+ context,
2088
2147
  );
2089
2148
  if (diagnosticMessage) {
2090
2149
  return { content: diagnosticMessage, isError: true };
@@ -2140,6 +2199,7 @@ export async function executeBrowserHover(
2140
2199
  const diagnosticMessage = formatCdpSendDiagnostics(
2141
2200
  err,
2142
2201
  acquired.browserMode,
2202
+ context,
2143
2203
  );
2144
2204
  if (diagnosticMessage) {
2145
2205
  return { content: diagnosticMessage, isError: true };
@@ -2235,6 +2295,7 @@ export async function executeBrowserWaitFor(
2235
2295
  const diagnosticMessage = formatCdpSendDiagnostics(
2236
2296
  err,
2237
2297
  acquired.browserMode,
2298
+ context,
2238
2299
  );
2239
2300
  if (diagnosticMessage) {
2240
2301
  return { content: diagnosticMessage, isError: true };
@@ -2320,6 +2381,7 @@ export async function executeBrowserExtract(
2320
2381
  const diagnosticMessage = formatCdpSendDiagnostics(
2321
2382
  err,
2322
2383
  acquired.browserMode,
2384
+ context,
2323
2385
  );
2324
2386
  if (diagnosticMessage) {
2325
2387
  return { content: diagnosticMessage, isError: true };
@@ -2455,6 +2517,7 @@ export async function executeBrowserFillCredential(
2455
2517
  const diagnosticMessage = formatCdpSendDiagnostics(
2456
2518
  err,
2457
2519
  acquired.browserMode,
2520
+ context,
2458
2521
  );
2459
2522
  if (diagnosticMessage) {
2460
2523
  return { content: diagnosticMessage, isError: true };
@@ -2490,6 +2553,7 @@ function modeTradeoffs(mode: StatusCheckMode): string[] {
2490
2553
  function extensionConnectionActions(): string[] {
2491
2554
  return [
2492
2555
  CHROME_EXTENSION_INSTALL_HINT,
2556
+ DESKTOP_APP_INSTALL_HINT,
2493
2557
  "Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
2494
2558
  ];
2495
2559
  }
@@ -2586,7 +2650,7 @@ async function probePinnedBrowserMode(
2586
2650
  return {
2587
2651
  ok: false,
2588
2652
  error: err,
2589
- diagnostic: formatModeSelectionFailure(mode, err),
2653
+ diagnostic: formatModeSelectionFailure(mode, err, context),
2590
2654
  };
2591
2655
  }
2592
2656
  const wrapped = new CdpError(
@@ -2597,7 +2661,7 @@ async function probePinnedBrowserMode(
2597
2661
  return {
2598
2662
  ok: false,
2599
2663
  error: wrapped,
2600
- diagnostic: formatModeSelectionFailure(mode, wrapped),
2664
+ diagnostic: formatModeSelectionFailure(mode, wrapped, context),
2601
2665
  };
2602
2666
  } finally {
2603
2667
  cdp?.dispose();
@@ -20,12 +20,12 @@ export const BROWSER_STATUS_INPUT_FIELD = {
20
20
  CHECK_LOCAL_LAUNCH: "check_local_launch",
21
21
  } as const;
22
22
 
23
- /** Chrome Web Store listing for the Vellum Assistant browser extension. */
24
- export const CHROME_WEB_STORE_INSTALL_URL =
25
- "https://chromewebstore.google.com/detail/vellum-assistant-browser/hphbdmpffeigpcdjkckleobjmhhokpne";
26
-
27
- /** User-facing install step for status userActions and command-failure hints. */
28
- export const CHROME_EXTENSION_INSTALL_HINT = `Install the Vellum Assistant Chrome extension from the Chrome Web Store: ${CHROME_WEB_STORE_INSTALL_URL}`;
23
+ export {
24
+ CHROME_EXTENSION_INSTALL_HINT,
25
+ CHROME_WEB_STORE_INSTALL_URL,
26
+ DESKTOP_APP_DOWNLOAD_URL,
27
+ DESKTOP_APP_INSTALL_HINT,
28
+ } from "../capability-offer.js";
29
29
 
30
30
  export const CDP_INSPECT_STATUS_DISCOVERY_CODE = {
31
31
  NO_TARGETS: DEVTOOLS_DISCOVERY_CODE.NO_TARGETS,
@@ -0,0 +1,87 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ appendLoggedInBrowserOffer,
5
+ CHROME_WEB_STORE_INSTALL_URL,
6
+ DESKTOP_APP_DOWNLOAD_URL,
7
+ formatLoggedInBrowserOffer,
8
+ isPhoneSurface,
9
+ shouldOfferLoggedInBrowser,
10
+ } from "./capability-offer.js";
11
+
12
+ describe("isPhoneSurface", () => {
13
+ test("is true for iOS transport and mobile client OS", () => {
14
+ expect(isPhoneSurface({ transportInterface: "ios" })).toBe(true);
15
+ expect(isPhoneSurface({ transportInterface: "phone" })).toBe(true);
16
+ expect(isPhoneSurface({ clientOs: "ios" })).toBe(true);
17
+ expect(isPhoneSurface({ clientOs: "android" })).toBe(true);
18
+ });
19
+
20
+ test("is false for desktop and web surfaces", () => {
21
+ expect(isPhoneSurface({ transportInterface: "web" })).toBe(false);
22
+ expect(isPhoneSurface({ transportInterface: "macos" })).toBe(false);
23
+ expect(isPhoneSurface({ clientOs: "macos" })).toBe(false);
24
+ expect(isPhoneSurface({})).toBe(false);
25
+ expect(isPhoneSurface()).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe("shouldOfferLoggedInBrowser", () => {
30
+ test("matches timeouts, unreachable hosts, and auth walls", () => {
31
+ expect(
32
+ shouldOfferLoggedInBrowser(
33
+ "Navigation to https://intranet.example.com timed out after 15000ms",
34
+ ),
35
+ ).toBe(true);
36
+ expect(shouldOfferLoggedInBrowser("net::ERR_CONNECTION_TIMED_OUT")).toBe(
37
+ true,
38
+ );
39
+ expect(shouldOfferLoggedInBrowser("ENOTFOUND intranet.example.com")).toBe(
40
+ true,
41
+ );
42
+ expect(shouldOfferLoggedInBrowser("host unreachable")).toBe(true);
43
+ expect(shouldOfferLoggedInBrowser("login required")).toBe(true);
44
+ expect(shouldOfferLoggedInBrowser("HTTP 403 Forbidden")).toBe(true);
45
+ });
46
+
47
+ test("does not match cancelled or invalid-input failures", () => {
48
+ expect(shouldOfferLoggedInBrowser("operation was cancelled")).toBe(false);
49
+ expect(shouldOfferLoggedInBrowser('Invalid browser_mode "bogus"')).toBe(
50
+ false,
51
+ );
52
+ expect(shouldOfferLoggedInBrowser("url must use http or https")).toBe(false);
53
+ });
54
+ });
55
+
56
+ describe("formatLoggedInBrowserOffer", () => {
57
+ test("offers both install links and prefers them over a screenshot", () => {
58
+ const offer = formatLoggedInBrowserOffer({ transportInterface: "web" });
59
+ expect(offer).toContain(DESKTOP_APP_DOWNLOAD_URL);
60
+ expect(offer).toContain(CHROME_WEB_STORE_INSTALL_URL);
61
+ expect(offer).toContain("Offer those first");
62
+ expect(offer).toContain("screenshot");
63
+ expect(offer).not.toContain("daemon");
64
+ expect(offer).not.toContain("browser panel");
65
+ });
66
+
67
+ test("is honest on a phone: no in-app browser, install on a computer", () => {
68
+ const offer = formatLoggedInBrowserOffer({
69
+ transportInterface: "ios",
70
+ clientOs: "ios",
71
+ });
72
+ expect(offer).toContain("no in-app browser");
73
+ expect(offer).toContain("Do not describe a browser panel");
74
+ expect(offer).toContain(DESKTOP_APP_DOWNLOAD_URL);
75
+ expect(offer).toContain(CHROME_WEB_STORE_INSTALL_URL);
76
+ });
77
+ });
78
+
79
+ describe("appendLoggedInBrowserOffer", () => {
80
+ test("appends the offer once", () => {
81
+ const once = appendLoggedInBrowserOffer("Error: Navigation failed: timeout");
82
+ expect(once).toContain("Error: Navigation failed: timeout");
83
+ expect(once).toContain(DESKTOP_APP_DOWNLOAD_URL);
84
+ const twice = appendLoggedInBrowserOffer(once);
85
+ expect(twice).toBe(once);
86
+ });
87
+ });