@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231715.8da971c

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 (229) hide show
  1. package/Dockerfile +7 -0
  2. package/README.md +12 -12
  3. package/docker/mousepad/90-vellum.gschema.override +4 -0
  4. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  5. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  6. package/openapi.yaml +97 -4
  7. package/package.json +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +4 -2
  9. package/src/__tests__/call-routes-http.test.ts +4 -2
  10. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  11. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +1 -0
  13. package/src/__tests__/conversation-pairing.test.ts +10 -0
  14. package/src/__tests__/conversation-resurface-done.test.ts +289 -0
  15. package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
  16. package/src/__tests__/gemini-provider.test.ts +31 -0
  17. package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
  18. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  19. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  20. package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
  21. package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
  22. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  23. package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
  24. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  25. package/src/__tests__/pricing.test.ts +33 -0
  26. package/src/__tests__/provider-error-scenarios.test.ts +46 -0
  27. package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
  28. package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
  29. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  30. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  31. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
  32. package/src/__tests__/send-endpoint-busy.test.ts +4 -2
  33. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  34. package/src/__tests__/skill-load-tool.test.ts +42 -11
  35. package/src/__tests__/skill-tool-factory.test.ts +50 -2
  36. package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
  37. package/src/__tests__/validate-input.test.ts +73 -0
  38. package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
  39. package/src/api/ask-question-tool.ts +50 -0
  40. package/src/api/events/tool-result.ts +11 -0
  41. package/src/api/index.ts +7 -0
  42. package/src/api/requests/dictation.ts +2 -2
  43. package/src/api/responses/home.ts +27 -0
  44. package/src/avatar/notification-avatar.test.ts +42 -34
  45. package/src/avatar/notification-avatar.ts +1 -0
  46. package/src/calls/__tests__/call-controller.test.ts +18 -0
  47. package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
  48. package/src/calls/__tests__/voice-leg-coordinator.test.ts +42 -0
  49. package/src/calls/__tests__/voice-session-bridge.test.ts +97 -4
  50. package/src/calls/__tests__/voice-triage-escalate.test.ts +69 -11
  51. package/src/calls/call-controller.ts +3 -1
  52. package/src/calls/voice-control-protocol.ts +9 -4
  53. package/src/calls/voice-escalation-judge.ts +7 -6
  54. package/src/calls/voice-leg-coordinator.ts +15 -4
  55. package/src/calls/voice-screen-annotation.ts +44 -0
  56. package/src/calls/voice-session-bridge.ts +50 -20
  57. package/src/calls/voice-triage-escalate.ts +61 -26
  58. package/src/cli/commands/__tests__/plugins.test.ts +29 -1
  59. package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
  60. package/src/cli/commands/oauth/index.help.ts +8 -2
  61. package/src/cli/commands/oauth/request.test.ts +32 -0
  62. package/src/cli/commands/platform/index.help.ts +2 -1
  63. package/src/cli/commands/platform/index.ts +8 -2
  64. package/src/cli/commands/plugins.ts +16 -0
  65. package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
  66. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
  67. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
  68. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  69. package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
  70. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
  71. package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
  72. package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
  73. package/src/cli/lib/bundled-marketplace.json +4 -4
  74. package/src/cli/lib/inspect-plugin.ts +5 -0
  75. package/src/cli/lib/plugin-catalog-cache.ts +41 -4
  76. package/src/cli/lib/plugin-catalog-local.ts +9 -0
  77. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  78. package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
  79. package/src/cli/lib/plugin-details.ts +10 -7
  80. package/src/cli/lib/search-plugins.ts +2 -0
  81. package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
  82. package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
  83. package/src/config/__tests__/platform-identity.test.ts +59 -0
  84. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  85. package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
  86. package/src/config/call-site-defaults.ts +17 -11
  87. package/src/config/default-profile-catalog.ts +118 -19
  88. package/src/config/default-profile-names.ts +69 -0
  89. package/src/config/feature-flag-registry.json +25 -9
  90. package/src/config/inference-profile-validation.ts +7 -6
  91. package/src/config/llm-resolver.ts +16 -0
  92. package/src/config/platform-identity.ts +22 -5
  93. package/src/config/profile-text-generation.test.ts +9 -0
  94. package/src/config/schemas/call-site-catalog.ts +8 -1
  95. package/src/config/schemas/llm.ts +70 -9
  96. package/src/config/sidebar-done-gate.ts +27 -0
  97. package/src/config/sync-gated-profiles.ts +96 -44
  98. package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
  99. package/src/daemon/auto-profile-router.ts +220 -0
  100. package/src/daemon/conversation-agent-loop.ts +77 -4
  101. package/src/daemon/conversation-slash.ts +2 -2
  102. package/src/daemon/conversation-turn-finalize.ts +12 -0
  103. package/src/daemon/dictation-text-processing.ts +4 -4
  104. package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
  105. package/src/daemon/handlers/config-channels.ts +3 -1
  106. package/src/daemon/handlers/conversation-history.ts +11 -1
  107. package/src/daemon/handlers/conversations.ts +118 -10
  108. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  109. package/src/daemon/pkb-reminder-builder.ts +1 -1
  110. package/src/desktop/desktop-browser-endpoint.ts +1 -0
  111. package/src/home/feed-types.ts +2 -0
  112. package/src/inbound/platform-callback-registration.ts +19 -6
  113. package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
  114. package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
  115. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  116. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +37 -1
  117. package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
  118. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  119. package/src/live-voice/continuation-judge.ts +1 -1
  120. package/src/live-voice/live-voice-session.ts +17 -14
  121. package/src/live-voice/protocol.ts +16 -5
  122. package/src/messaging/providers/gmail/client.ts +4 -0
  123. package/src/notifications/README.md +11 -3
  124. package/src/notifications/__tests__/assistant-reply-producer.test.ts +13 -0
  125. package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
  126. package/src/notifications/conversation-pairing.ts +23 -8
  127. package/src/notifications/home-feed-mirror.ts +39 -0
  128. package/src/notifications/home-feed-side-effect.ts +46 -19
  129. package/src/oauth/AGENTS.md +9 -1
  130. package/src/oauth/byo-connection.test.ts +28 -0
  131. package/src/permissions/question-prompter.ts +14 -4
  132. package/src/persistence/conversation-crud.ts +116 -3
  133. package/src/persistence/conversation-queries.ts +48 -3
  134. package/src/persistence/conversation-types.ts +6 -2
  135. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
  136. package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
  137. package/src/persistence/embeddings/embedding-backend.ts +23 -41
  138. package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
  139. package/src/persistence/embeddings/embedding-gemini.ts +3 -1
  140. package/src/persistence/embeddings/embedding-local.ts +72 -17
  141. package/src/persistence/embeddings/embedding-types.ts +0 -19
  142. package/src/persistence/jobs-store.ts +229 -0
  143. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  144. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  145. package/src/persistence/steps.ts +12 -0
  146. package/src/platform/sync-avatar.test.ts +29 -0
  147. package/src/plugin-api/model-profiles.ts +5 -3
  148. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  149. package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
  150. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  151. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  152. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  153. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  154. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  155. package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
  156. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  157. package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
  158. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  159. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
  160. package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
  161. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  162. package/src/providers/anthropic/client.ts +2 -2
  163. package/src/providers/gemini/client.ts +17 -1
  164. package/src/providers/inference/auth.ts +25 -0
  165. package/src/providers/jev/ask.test.ts +66 -0
  166. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
  167. package/src/providers/malformed-tool-call.ts +55 -0
  168. package/src/providers/model-catalog.ts +257 -4
  169. package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
  170. package/src/providers/openai/chat-completions-provider.ts +30 -0
  171. package/src/providers/openai/codex-models.ts +2 -0
  172. package/src/providers/platform-proxy/constants.ts +5 -0
  173. package/src/providers/retry.ts +68 -36
  174. package/src/providers/search-provider-catalog.ts +10 -0
  175. package/src/providers/vellum-model-routing.test.ts +1 -0
  176. package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
  177. package/src/runtime/__tests__/question-resolution.test.ts +32 -0
  178. package/src/runtime/agent-wake.ts +9 -2
  179. package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
  180. package/src/runtime/auth/middleware.ts +15 -11
  181. package/src/runtime/middleware/auth.ts +7 -1
  182. package/src/runtime/pending-interactions.ts +8 -8
  183. package/src/runtime/question-resolution.ts +54 -8
  184. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
  185. package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
  186. package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
  187. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  188. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
  189. package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
  190. package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
  191. package/src/runtime/routes/conversation-list-routes.ts +16 -14
  192. package/src/runtime/routes/conversation-query-routes.ts +4 -0
  193. package/src/runtime/routes/conversation-routes.ts +4 -2
  194. package/src/runtime/routes/diagnostics-routes.ts +66 -229
  195. package/src/runtime/routes/global-search-routes.ts +22 -7
  196. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  197. package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
  198. package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
  199. package/src/runtime/routes/platform-routes.ts +6 -1
  200. package/src/runtime/routes/plugins-routes.ts +5 -2
  201. package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
  202. package/src/runtime/sync/resource-sync-events.ts +11 -0
  203. package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
  204. package/src/runtime/sync/worker-daemon-notify.ts +44 -0
  205. package/src/runtime/verification-outbound-actions.ts +2 -2
  206. package/src/skills/validate-input.ts +75 -11
  207. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  208. package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
  209. package/src/tools/ask-question/ask-question-tool.ts +12 -1
  210. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  211. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  212. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  213. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  214. package/src/tools/network/web-fetch.ts +30 -9
  215. package/src/tools/network/web-search.ts +235 -4
  216. package/src/tools/skills/execute.ts +1 -1
  217. package/src/tools/skills/format-input-schema.ts +15 -0
  218. package/src/tools/skills/load.ts +15 -43
  219. package/src/tools/skills/scaffold-managed.ts +86 -71
  220. package/src/tools/skills/skill-tool-factory.ts +2 -1
  221. package/src/tools/workflows/manage-workflows.ts +4 -2
  222. package/src/util/oauth-request-body.test.ts +5 -1
  223. package/src/util/worker-pipe.test.ts +146 -0
  224. package/src/util/worker-pipe.ts +49 -0
  225. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  226. package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
  227. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  228. package/src/workspace/migrations/registry.ts +4 -0
  229. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
@@ -7,15 +7,17 @@
7
7
  * catalog sourced from the platform is indistinguishable from one read off
8
8
  * GitHub.
9
9
  *
10
- * Every failure mode (non-2xx, unreachable/aborted, malformed body) throws
11
- * {@link PluginCatalogUnavailableError} — the fetcher never returns a partial
12
- * or silently-empty catalog, so a caller can safely fall back to a bundled
13
- * offline copy instead of mistaking an outage for "no plugins".
10
+ * Every failure mode (non-2xx, unreachable/aborted, malformed envelope)
11
+ * throws {@link PluginCatalogUnavailableError}, so a caller never mistakes an
12
+ * outage for "no plugins". Rows are validated one at a time: a row with an
13
+ * invalid `integration` is kept without it, and a row that fails the base
14
+ * row schema is skipped, so one bad row never blanks the whole catalog.
14
15
  */
15
16
 
16
17
  import { z } from "zod";
17
18
 
18
19
  import { getPlatformBaseUrl } from "../../config/env.js";
20
+ import { getLogger } from "../../util/logger.js";
19
21
  import {
20
22
  type MarketplaceEntry,
21
23
  mcpIntegrationSchema,
@@ -27,9 +29,12 @@ import {
27
29
  type SearchPluginsDeps,
28
30
  } from "./search-plugins.js";
29
31
 
32
+ const log = getLogger("plugin-catalog");
33
+
30
34
  /**
31
35
  * One flattened row from the platform catalog. Unknown keys (`id`,
32
- * `display_name`) are accepted and dropped — zod strips them by default.
36
+ * `display_name`) are accepted and dropped (zod strips them by default).
37
+ * `integration` is validated separately by {@link integrationSchema}.
33
38
  */
34
39
  const platformPluginRowSchema = z.object({
35
40
  name: z.string(),
@@ -41,11 +46,13 @@ const platformPluginRowSchema = z.object({
41
46
  category: z.string().nullable().optional(),
42
47
  homepage: z.string().nullable().optional(),
43
48
  license: z.string().nullable().optional(),
44
- integration: mcpIntegrationSchema.nullable().optional(),
49
+ integration: z.unknown(),
45
50
  });
46
51
 
52
+ const integrationSchema = mcpIntegrationSchema.nullable().optional();
53
+
47
54
  const platformCatalogSchema = z.object({
48
- plugins: z.array(platformPluginRowSchema),
55
+ plugins: z.array(z.unknown()),
49
56
  });
50
57
 
51
58
  /**
@@ -99,10 +106,22 @@ export async function fetchPluginCatalogFromPlatform(
99
106
  }
100
107
 
101
108
  const entries: MarketplaceEntry[] = [];
102
- for (const row of parsed.data.plugins) {
109
+ const skippedRows: string[] = [];
110
+ const droppedIntegrations: string[] = [];
111
+ for (const [index, rawRow] of parsed.data.plugins.entries()) {
112
+ const rowResult = platformPluginRowSchema.safeParse(rawRow);
113
+ if (!rowResult.success) {
114
+ skippedRows.push(describeInvalidRow(rawRow, index));
115
+ continue;
116
+ }
117
+ const row = rowResult.data;
103
118
  if (!row.repo || !row.ref) {
104
119
  continue;
105
120
  }
121
+ const integrationResult = integrationSchema.safeParse(row.integration);
122
+ if (!integrationResult.success) {
123
+ droppedIntegrations.push(row.name);
124
+ }
106
125
  entries.push({
107
126
  name: row.name,
108
127
  source: {
@@ -116,12 +135,32 @@ export async function fetchPluginCatalogFromPlatform(
116
135
  category: row.category ?? undefined,
117
136
  homepage: row.homepage ?? undefined,
118
137
  license: row.license ?? undefined,
119
- integration: row.integration ?? undefined,
138
+ integration: integrationResult.success
139
+ ? (integrationResult.data ?? undefined)
140
+ : undefined,
120
141
  });
121
142
  }
122
143
 
144
+ if (skippedRows.length > 0 || droppedIntegrations.length > 0) {
145
+ log.warn(
146
+ { skippedRows, droppedIntegrations },
147
+ "platform plugin catalog contained invalid rows; skipped rows and dropped integrations",
148
+ );
149
+ }
150
+
123
151
  return {
124
152
  ref: opts?.ref ?? "platform",
125
153
  matches: projectMarketplaceEntries(entries),
126
154
  };
127
155
  }
156
+
157
+ /** Name of an invalid row for logging, or its position when it has none. */
158
+ function describeInvalidRow(row: unknown, index: number): string {
159
+ if (row && typeof row === "object" && "name" in row) {
160
+ const name = (row as { name: unknown }).name;
161
+ if (typeof name === "string" && name.length > 0) {
162
+ return name;
163
+ }
164
+ }
165
+ return `#${index}`;
166
+ }
@@ -0,0 +1,41 @@
1
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
2
+ import type { PluginCatalog } from "./search-plugins.js";
3
+
4
+ export const MCP_CATALOG_QA_INTEGRATIONS_FLAG = "mcp-catalog-qa-integrations";
5
+
6
+ const MCP_CATALOG_QA_INTEGRATION_NAMES = new Set([
7
+ "gamma",
8
+ "guru",
9
+ "intercom",
10
+ "ramp",
11
+ "semrush",
12
+ "typeform",
13
+ ]);
14
+
15
+ export type PluginCatalogFeatureFlagResolver = (key: string) => boolean;
16
+
17
+ export function isPluginCatalogEntryVisible(
18
+ name: string,
19
+ isFeatureFlagEnabled: PluginCatalogFeatureFlagResolver = isAssistantFeatureFlagEnabled,
20
+ ): boolean {
21
+ return (
22
+ !MCP_CATALOG_QA_INTEGRATION_NAMES.has(name) ||
23
+ isFeatureFlagEnabled(MCP_CATALOG_QA_INTEGRATIONS_FLAG)
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Hide catalog entries whose OAuth flows are still being validated. Installed
29
+ * plugins are unaffected because this filter only applies to catalog reads.
30
+ */
31
+ export function filterPluginCatalogByFeatureFlags(
32
+ catalog: PluginCatalog,
33
+ isFeatureFlagEnabled: PluginCatalogFeatureFlagResolver = isAssistantFeatureFlagEnabled,
34
+ ): PluginCatalog {
35
+ const matches = catalog.matches.filter((match) =>
36
+ isPluginCatalogEntryVisible(match.name, isFeatureFlagEnabled),
37
+ );
38
+ return matches.length === catalog.matches.length
39
+ ? catalog
40
+ : { ...catalog, matches };
41
+ }
@@ -46,6 +46,7 @@ import {
46
46
  findCatalogEntry,
47
47
  resolveSourceFromMatch,
48
48
  } from "./plugin-catalog-resolve.js";
49
+ import { isPluginCatalogEntryVisible } from "./plugin-catalog-visibility.js";
49
50
  import { DEFAULT_PLUGIN_REF } from "./plugin-constants.js";
50
51
  import { readValidatedPluginIcon } from "./plugin-icon-file.js";
51
52
  import { fetchMarketplaceEntries } from "./plugin-marketplace.js";
@@ -156,8 +157,8 @@ export class PluginDetailsNotFoundError extends Error {
156
157
  * Resolve the detail view for {@link opts.name}.
157
158
  *
158
159
  * Throws {@link PluginDetailsNotFoundError} when the name is neither installed
159
- * locally nor present in the catalog (gated at the default ref, the GitHub
160
- * marketplace at an explicit historical ref). A gated-catalog outage
160
+ * locally nor present in the visibility-gated catalog at either the default
161
+ * or an explicit historical ref. A gated-catalog outage
161
162
  * ({@link PluginCatalogUnavailableError}) degrades to the on-disk fields only
162
163
  * when a local copy exists; with nothing installed to render it propagates so
163
164
  * the caller can map the transient failure to a retryable 503 rather than a
@@ -400,11 +401,10 @@ async function fetchRawFile(
400
401
  /**
401
402
  * Resolve the external catalog entry claiming {@link name}.
402
403
  *
403
- * The default ref reads the gated catalog (the same source search / install
404
- * use). An explicit historical {@link ref} reads the GitHub marketplace
405
- * manifest at that revision — the gated catalog is ref-agnostic, so honoring a
406
- * reviewed/rolled-back revision is inherently a git lookup, matching the pin
407
- * history / inspect carve-out.
404
+ * Both default and historical-ref lookups apply catalog visibility first. The
405
+ * default ref then reads the gated catalog used by search and install, while an
406
+ * explicit historical {@link ref} reads the GitHub marketplace manifest at
407
+ * that revision.
408
408
  *
409
409
  * A gated-catalog outage (fail-hard {@link PluginCatalogUnavailableError})
410
410
  * propagates when nothing is installed — there is nothing to render and the
@@ -420,6 +420,9 @@ async function resolveCatalogEntry(
420
420
  fetchFn: FetchLike,
421
421
  installed: boolean,
422
422
  ): Promise<PluginSearchMatch | null> {
423
+ if (!isPluginCatalogEntryVisible(name)) {
424
+ return null;
425
+ }
423
426
  try {
424
427
  const match =
425
428
  ref === DEFAULT_PLUGIN_REF
@@ -19,6 +19,8 @@ import type { MarketplaceEntry } from "./plugin-marketplace.js";
19
19
  export interface SearchPluginsDeps {
20
20
  /** HTTP client. Production callers pass `globalThis.fetch.bind(globalThis)`. */
21
21
  readonly fetch: FetchLike;
22
+ /** Optional feature-flag resolver for deterministic tests. */
23
+ readonly featureFlagEnabled?: (key: string) => boolean;
22
24
  }
23
25
 
24
26
  /** Where a catalog match comes from. */
@@ -5,10 +5,12 @@ import { resolveModelIntent } from "../../providers/model-intents.js";
5
5
  import { CALL_SITE_DEFAULTS } from "../call-site-defaults.js";
6
6
  import {
7
7
  CODE_DEFAULT_PROFILE_ENTRIES,
8
+ getConversationProfilesForProvider,
8
9
  getEffectiveProfile,
9
10
  getEffectiveProfiles,
10
11
  getEffectiveProfilesForProvider,
11
12
  getUserSelectableProfilesForProvider,
13
+ MANAGED_PROFILE_TEMPLATES,
12
14
  PROFILE_IMPLS,
13
15
  resolveDefaultProfileForProvider,
14
16
  } from "../default-profile-catalog.js";
@@ -59,6 +61,64 @@ describe("getEffectiveProfiles", () => {
59
61
  }
60
62
  });
61
63
 
64
+ test("the managed Jev profile routes jev-latest through TypeSafe and is managed-only", () => {
65
+ const jev = CODE_DEFAULT_PROFILE_ENTRIES["jev-managed"];
66
+ expect(jev.model).toBe("jev-latest");
67
+ expect(jev.provider_connection).toBeUndefined();
68
+ expect(resolveRoutingIdentity(jev.provider, jev.model)).toEqual({
69
+ connectionName: "vellum",
70
+ expectedProvider: "typesafe",
71
+ });
72
+ expect(
73
+ resolveDefaultProfileForProvider(undefined, "jev-managed", null),
74
+ ).toBeDefined();
75
+ expect(
76
+ resolveDefaultProfileForProvider(undefined, "jev-managed", {
77
+ provider: "anthropic",
78
+ connectionName: "anthropic-personal",
79
+ }),
80
+ ).toBeUndefined();
81
+ });
82
+
83
+ test("the Auto profile is the managed Balanced body relabeled, managed-only, and flag-gated", () => {
84
+ const auto = CODE_DEFAULT_PROFILE_ENTRIES.auto;
85
+ const balanced = CODE_DEFAULT_PROFILE_ENTRIES.balanced;
86
+ expect(auto.label).toBe("Auto");
87
+ expect(auto.model).toBe(balanced.model);
88
+ expect(auto.provider).toBe(balanced.provider);
89
+ expect(auto.fallbackProfile).toBeUndefined();
90
+ // Not seeded unconditionally: the flag reconcile writes the stub.
91
+ expect(Object.keys(MANAGED_PROFILE_TEMPLATES)).not.toContain("auto");
92
+ expect(
93
+ resolveDefaultProfileForProvider(undefined, "auto", null),
94
+ ).toBeUndefined();
95
+ const stub: Record<string, ProfileEntry> = { auto: { source: "managed" } };
96
+ expect(resolveDefaultProfileForProvider(stub, "auto", null)?.model).toBe(
97
+ balanced.model,
98
+ );
99
+ expect(
100
+ resolveDefaultProfileForProvider(stub, "auto", {
101
+ provider: "anthropic",
102
+ connectionName: "anthropic-personal",
103
+ }),
104
+ ).toBeUndefined();
105
+ expect(
106
+ Object.keys(getConversationProfilesForProvider(stub, null)),
107
+ ).toContain("auto");
108
+ });
109
+
110
+ test("the conversation view drops the managed Jev profile but keeps it selectable for call sites", () => {
111
+ expect(
112
+ Object.keys(getConversationProfilesForProvider(undefined, null)),
113
+ ).not.toContain("jev-managed");
114
+ expect(
115
+ Object.keys(getUserSelectableProfilesForProvider(undefined, null)),
116
+ ).toContain("jev-managed");
117
+ expect(
118
+ Object.keys(getConversationProfilesForProvider(undefined, null)),
119
+ ).toContain("balanced");
120
+ });
121
+
62
122
  test("the managed Balanced profile routes GLM 5.3 Flash through Fireworks", () => {
63
123
  const balanced = CODE_DEFAULT_PROFILE_ENTRIES.balanced;
64
124
  expect(balanced.model).toBe("accounts/fireworks/models/glm-5p3-flash");
@@ -80,7 +140,7 @@ describe("getEffectiveProfiles", () => {
80
140
  test("defaults absent from the workspace resolve from the catalog; os-beta stays flag-gated", () => {
81
141
  const effective = getEffectiveProfiles(undefined);
82
142
  expect(Object.keys(effective).sort()).toEqual(
83
- [...DEFAULT_PROFILE_KEYS, ...BACKUP_PROFILE_KEYS].sort(),
143
+ [...DEFAULT_PROFILE_KEYS, ...BACKUP_PROFILE_KEYS, "jev-managed"].sort(),
84
144
  );
85
145
  expect(getEffectiveProfile({}, "balanced")?.model).toBe(
86
146
  CODE_DEFAULT_PROFILE_ENTRIES.balanced.model as string,
@@ -0,0 +1,82 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { LLMSchema } from "../schemas/llm.js";
4
+
5
+ const PIN = { callSites: { voiceEscalationJudge: { profile: "jev-managed" } } };
6
+
7
+ describe("jev-managed as a call-site pin target", () => {
8
+ test("resolves under the managed default provider", () => {
9
+ expect(LLMSchema.safeParse(PIN).success).toBe(true);
10
+ expect(
11
+ LLMSchema.safeParse({
12
+ ...PIN,
13
+ defaultProvider: { provider: "vellum" },
14
+ }).success,
15
+ ).toBe(true);
16
+ });
17
+
18
+ test("can be pinned only to the verdict-consuming call sites", () => {
19
+ for (const site of [
20
+ "voiceEscalationJudge",
21
+ "voiceContinuationJudge",
22
+ "memoryV3SelectL2",
23
+ ]) {
24
+ expect(
25
+ LLMSchema.safeParse({
26
+ callSites: { [site]: { profile: "jev-managed" } },
27
+ }).success,
28
+ ).toBe(true);
29
+ }
30
+ const result = LLMSchema.safeParse({
31
+ callSites: { conversationTitle: { profile: "jev-managed" } },
32
+ });
33
+ expect(result.success).toBe(false);
34
+ expect(JSON.stringify(result.error?.issues)).toContain(
35
+ "can only be pinned to",
36
+ );
37
+ });
38
+
39
+ test("cannot be a mix constituent", () => {
40
+ const result = LLMSchema.safeParse({
41
+ profiles: {
42
+ blend: {
43
+ mix: [
44
+ { profile: "jev-managed", weight: 1 },
45
+ { profile: "balanced", weight: 1 },
46
+ ],
47
+ },
48
+ },
49
+ });
50
+ expect(result.success).toBe(false);
51
+ expect(JSON.stringify(result.error?.issues)).toContain(
52
+ "cannot be a mix constituent",
53
+ );
54
+ });
55
+
56
+ test("is rejected as the active or advisor profile even on the managed column", () => {
57
+ for (const field of ["activeProfile", "advisorProfile"] as const) {
58
+ const result = LLMSchema.safeParse({ [field]: "jev-managed" });
59
+ expect(result.success).toBe(false);
60
+ expect(JSON.stringify(result.error?.issues)).toContain(
61
+ "structured answers rather than chat text",
62
+ );
63
+ }
64
+ expect(LLMSchema.safeParse({ activeProfile: "balanced" }).success).toBe(
65
+ true,
66
+ );
67
+ });
68
+
69
+ test("is rejected under a BYOK default provider, where it has no body", () => {
70
+ const result = LLMSchema.safeParse({
71
+ ...PIN,
72
+ defaultProvider: {
73
+ provider: "anthropic",
74
+ connectionName: "anthropic-personal",
75
+ },
76
+ });
77
+ expect(result.success).toBe(false);
78
+ expect(JSON.stringify(result.error?.issues)).toContain(
79
+ "managed Jev profile",
80
+ );
81
+ });
82
+ });
@@ -116,6 +116,7 @@ describe("fetchPlatformIdentityIds", () => {
116
116
  describe("resolvePlatformAssistantId", () => {
117
117
  const originalFetch = globalThis.fetch;
118
118
  const originalAssistantApiKeyEnv = process.env.ASSISTANT_API_KEY;
119
+ const originalPlatformUrlEnv = process.env.VELLUM_PLATFORM_URL;
119
120
  const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
120
121
  let fetchImpl: (
121
122
  input: RequestInfo | URL,
@@ -149,6 +150,11 @@ describe("resolvePlatformAssistantId", () => {
149
150
  } else {
150
151
  process.env.ASSISTANT_API_KEY = originalAssistantApiKeyEnv;
151
152
  }
153
+ if (originalPlatformUrlEnv === undefined) {
154
+ delete process.env.VELLUM_PLATFORM_URL;
155
+ } else {
156
+ process.env.VELLUM_PLATFORM_URL = originalPlatformUrlEnv;
157
+ }
152
158
  setPlatformBaseUrl(undefined);
153
159
  setPlatformAssistantId(undefined);
154
160
  setPlatformOrganizationId(undefined);
@@ -198,6 +204,59 @@ describe("resolvePlatformAssistantId", () => {
198
204
  expect(fetchCalls).toHaveLength(1);
199
205
  });
200
206
 
207
+ test("retries validate inside the cooldown once a different API key is stored", async () => {
208
+ process.env.ASSISTANT_API_KEY = "rejected-key";
209
+ setPlatformBaseUrl(BASE_URL);
210
+ fetchImpl = async () => new Response("no", { status: 401 });
211
+
212
+ await expect(resolvePlatformAssistantId()).resolves.toBe("");
213
+ expect(fetchCalls).toHaveLength(1);
214
+
215
+ process.env.ASSISTANT_API_KEY = "replacement-key";
216
+ fetchImpl = async () =>
217
+ new Response(
218
+ JSON.stringify({
219
+ assistant_id: ASSISTANT_ID,
220
+ organization_id: ORG_ID,
221
+ user_id: USER_ID,
222
+ }),
223
+ { status: 200 },
224
+ );
225
+
226
+ await expect(resolvePlatformAssistantId()).resolves.toBe(ASSISTANT_ID);
227
+ expect(fetchCalls).toHaveLength(2);
228
+ const headers = new Headers(fetchCalls[1]?.init?.headers);
229
+ expect(headers.get("Authorization")).toBe("Api-Key replacement-key");
230
+ });
231
+
232
+ test("retries validate inside the cooldown once a different base URL is stored", async () => {
233
+ process.env.ASSISTANT_API_KEY = "assistant-key";
234
+ // The environment URL outranks the in-memory override (and the test
235
+ // preload sets one), so the base URL is changed through the environment.
236
+ process.env.VELLUM_PLATFORM_URL = "https://old.example.com";
237
+ fetchImpl = async () => new Response("no", { status: 401 });
238
+
239
+ await expect(resolvePlatformAssistantId()).resolves.toBe("");
240
+ expect(fetchCalls).toHaveLength(1);
241
+
242
+ process.env.VELLUM_PLATFORM_URL = BASE_URL;
243
+ fetchImpl = async () =>
244
+ new Response(
245
+ JSON.stringify({
246
+ assistant_id: ASSISTANT_ID,
247
+ organization_id: ORG_ID,
248
+ user_id: USER_ID,
249
+ }),
250
+ { status: 200 },
251
+ );
252
+
253
+ await expect(resolvePlatformAssistantId()).resolves.toBe(ASSISTANT_ID);
254
+ expect(fetchCalls).toHaveLength(2);
255
+ expect(fetchCalls[1]?.url).toBe(
256
+ `${BASE_URL}${PLATFORM_IDENTITY_VALIDATE_PATH}`,
257
+ );
258
+ });
259
+
201
260
  test("retries validate after a failed attempt once the cooldown is cleared", async () => {
202
261
  process.env.ASSISTANT_API_KEY = "assistant-key";
203
262
  setPlatformBaseUrl(BASE_URL);
@@ -368,4 +368,75 @@ describe("reconcileFlagGatedProfiles", () => {
368
368
  expect(reconcileFlagGatedProfiles()).toBe(false);
369
369
  expect(readFileSync(CONFIG_PATH, "utf-8")).toBe(before);
370
370
  });
371
+
372
+ test("auto-profile flag on materializes the managed auto profile at the head of the order", () => {
373
+ process.env.IS_PLATFORM = "true";
374
+ seedBalancedConfig();
375
+ setOverridesForTesting({ "auto-profile": true });
376
+
377
+ expect(reconcileFlagGatedProfiles()).toBe(true);
378
+
379
+ const raw = readConfig();
380
+ expect(raw.llm.profiles.auto).toEqual({ source: "managed" });
381
+ expect(raw.llm.profileOrder[0]).toBe("auto");
382
+ const effective = getEffectiveProfile(raw.llm.profiles, "auto")!;
383
+ expect(effective.label).toBe("Auto");
384
+ expect(effective.model).toBe(
385
+ getEffectiveProfile(raw.llm.profiles, "balanced")!.model,
386
+ );
387
+ expect(LLMSchema.safeParse(raw.llm).success).toBe(true);
388
+ });
389
+
390
+ test("auto-profile flag on in BYOK mode writes a plain stub that the effective view hides", () => {
391
+ seedBalancedConfig();
392
+ setOverridesForTesting({ "auto-profile": true });
393
+
394
+ expect(reconcileFlagGatedProfiles()).toBe(true);
395
+
396
+ const raw = readConfig();
397
+ expect(raw.llm.profiles.auto).toEqual({ source: "managed" });
398
+ expect(getEffectiveProfile(raw.llm.profiles, "auto")).toBeDefined();
399
+ });
400
+
401
+ test("auto-profile flag off removes the managed auto profile and repoints its references", () => {
402
+ process.env.IS_PLATFORM = "true";
403
+ seedBalancedConfig();
404
+ setOverridesForTesting({ "auto-profile": true });
405
+ reconcileFlagGatedProfiles();
406
+
407
+ const raw = readConfig();
408
+ raw.llm.activeProfile = "auto";
409
+ (raw.llm as Record<string, unknown>).callSites = {
410
+ subagentSpawn: { profile: "auto" },
411
+ };
412
+ writeConfig(raw);
413
+ invalidateConfigCache();
414
+
415
+ setOverridesForTesting({ "auto-profile": false });
416
+ expect(reconcileFlagGatedProfiles()).toBe(true);
417
+
418
+ const after = readConfig();
419
+ expect(after.llm.profiles.auto).toBeUndefined();
420
+ expect(after.llm.profileOrder.includes("auto")).toBe(false);
421
+ expect(after.llm.activeProfile).toBe("balanced");
422
+ expect((after.llm as Record<string, unknown>).callSites).toEqual({
423
+ subagentSpawn: {},
424
+ });
425
+ });
426
+
427
+ test("os-beta and auto-profile reconcile independently in one pass", () => {
428
+ process.env.IS_PLATFORM = "true";
429
+ seedBalancedConfig();
430
+ setOverridesForTesting({ "os-beta": true, "auto-profile": true });
431
+ expect(reconcileFlagGatedProfiles()).toBe(true);
432
+ let order = readConfig().llm.profileOrder;
433
+ expect(order[0]).toBe("auto");
434
+ expect(order.indexOf("os-beta")).toBe(order.indexOf("balanced") + 1);
435
+
436
+ setOverridesForTesting({ "os-beta": false, "auto-profile": true });
437
+ expect(reconcileFlagGatedProfiles()).toBe(true);
438
+ order = readConfig().llm.profileOrder;
439
+ expect(order[0]).toBe("auto");
440
+ expect(order.includes("os-beta")).toBe(false);
441
+ });
371
442
  });
@@ -46,9 +46,12 @@ nothing, because no control is called that. Give the label on its own:
46
46
  The arrow points at the middle of the control and stops just short, so what
47
47
  you are sending someone to stays visible the whole time.
48
48
 
49
- You are answered with what was drawn and the name it resolved to, which is not
50
- always the name you asked for. Say the resolved one out loud: it is the word
51
- the user can see.
49
+ You are answered with what was drawn and the accessibility name it resolved
50
+ to. Some apps expose internal names such as `ToolbarExportControl` instead of
51
+ the visible label. When a returned name identifies the intended control in
52
+ the shared image, use that exact name as `target`; use the visible label in
53
+ the caption and speech. Do not invent an internal name or choose an unclear
54
+ candidate.
52
55
 
53
56
  A name the surface does not carry draws nothing and comes back with the names
54
57
  it does carry. That is the answer, not a setback: the thing is nearly always
@@ -1,3 +1,4 @@
1
+ import { JEV_MANAGED_PROFILE_KEY } from "./default-profile-names.js";
1
2
  import { type LLMCallSite } from "./schemas/llm.js";
2
3
 
3
4
  type CallSiteDefaultConfig = {
@@ -123,9 +124,8 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
123
124
  effort: "low",
124
125
  thinking: { enabled: false },
125
126
  },
126
- // The dictation cleanup pass, and nothing else runs on this site. The
127
- // caller has already stopped speaking and has nothing on screen until this
128
- // answers, so it is priced as a latency call.
127
+ // Voice intent classification and explicit selected-text edits are
128
+ // latency-sensitive: the caller is waiting for insertion or dispatch.
129
129
  interactionClassifier: {
130
130
  profile: "latency-optimized",
131
131
  effort: "low",
@@ -153,14 +153,20 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
153
153
  effort: "low",
154
154
  thinking: { enabled: false },
155
155
  },
156
- // The escalation judge only runs when this site resolves to the TypeSafe
157
- // provider (see calls/voice-escalation-judge.ts), so the shipped default is
158
- // any ordinary profile: the judge stays off until a user pins a TypeSafe
159
- // profile here.
160
- voiceEscalationJudge: { profile: "cost-optimized" },
161
- // Same arrangement for the barge-in continuation judge
162
- // (live-voice/continuation-judge.ts): off until a TypeSafe profile is
163
- // pinned here.
156
+ // The managed-only profile resolves through the Vellum connection. On a
157
+ // BYOK install it has no body, so the judge is unavailable and the front
158
+ // door's own decision stands.
159
+ voiceEscalationJudge: {
160
+ profile: JEV_MANAGED_PROFILE_KEY,
161
+ },
162
+ // Picks the default profile for a turn pinned to the Auto profile. On a
163
+ // BYOK install the managed-only Jev profile has no body, so the router is
164
+ // unavailable and the turn runs on Balanced.
165
+ autoProfileRouter: {
166
+ profile: JEV_MANAGED_PROFILE_KEY,
167
+ },
168
+ // The barge-in continuation judge remains off until a TypeSafe profile is
169
+ // pinned here (see live-voice/continuation-judge.ts).
164
170
  voiceContinuationJudge: { profile: "cost-optimized" },
165
171
  // Names the background continuation a barge-in spawns, from the interrupted
166
172
  // transcript. The label is fixed at spawn, so the call runs under a short