@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa

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 (295) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +26 -3
  3. package/docs/automatic-done.md +32 -0
  4. package/docs/desktop-browser-cli.md +3 -2
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
  10. package/node_modules/@vellumai/environments/src/shell.ts +22 -1
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  15. package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
  16. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
  17. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  18. package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  19. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  21. package/openapi.yaml +270 -10
  22. package/package.json +1 -1
  23. package/src/__tests__/agent-loop.test.ts +76 -0
  24. package/src/__tests__/anthropic-provider.test.ts +2 -0
  25. package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
  26. package/src/__tests__/config-get-vision-flag.test.ts +1 -1
  27. package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
  28. package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
  29. package/src/__tests__/conversation-evictor.test.ts +54 -0
  30. package/src/__tests__/conversation-pairing.test.ts +112 -0
  31. package/src/__tests__/document-append-idempotency.test.ts +13 -0
  32. package/src/__tests__/document-create-dedupe.test.ts +13 -0
  33. package/src/__tests__/document-find-replace.test.ts +13 -0
  34. package/src/__tests__/document-revision-history.test.ts +390 -0
  35. package/src/__tests__/document-revision.test.ts +425 -0
  36. package/src/__tests__/document-tool-security.test.ts +13 -0
  37. package/src/__tests__/document-update-default-surface.test.ts +13 -0
  38. package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
  39. package/src/__tests__/gateway-flag-listener.test.ts +59 -6
  40. package/src/__tests__/headless-browser-navigate.test.ts +20 -30
  41. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  42. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  43. package/src/__tests__/host-shell-tool.test.ts +6 -2
  44. package/src/__tests__/identity-routes.test.ts +4 -3
  45. package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
  46. package/src/__tests__/llm-catalog-parity.test.ts +4 -4
  47. package/src/__tests__/llm-schema.test.ts +1 -1
  48. package/src/__tests__/notification-platform-adapter.test.ts +83 -1
  49. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  50. package/src/__tests__/openai-provider.test.ts +38 -11
  51. package/src/__tests__/pricing.test.ts +11 -0
  52. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  53. package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
  54. package/src/__tests__/skill-execute-input.test.ts +11 -0
  55. package/src/__tests__/tool-approval-handler.test.ts +4 -0
  56. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
  57. package/src/__tests__/usage-attribution.test.ts +31 -0
  58. package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
  59. package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
  60. package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
  61. package/src/acp/agent-process.ts +6 -1
  62. package/src/acp/client-handler.ts +6 -1
  63. package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
  64. package/src/agent/loop.ts +161 -17
  65. package/src/api/events/document-editor-show.ts +5 -0
  66. package/src/api/events/document-editor-update.ts +10 -0
  67. package/src/api/events/tool-result.ts +1 -0
  68. package/src/calls/__tests__/call-controller.test.ts +273 -1
  69. package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
  70. package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
  71. package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
  72. package/src/calls/call-constants.ts +10 -0
  73. package/src/calls/call-controller.ts +359 -25
  74. package/src/calls/channel-admission-reader.ts +15 -9
  75. package/src/calls/media-stream-server.ts +18 -0
  76. package/src/calls/media-stream-stt-session.ts +30 -9
  77. package/src/calls/twilio-provider.ts +14 -31
  78. package/src/calls/voice-session-bridge.ts +18 -1
  79. package/src/cli/commands/browser.ts +1 -2
  80. package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
  81. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  82. package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
  83. package/src/cli/lib/bundled-marketplace.json +54 -2
  84. package/src/cli/lib/inspect-plugin.ts +15 -15
  85. package/src/cli/lib/list-installed-plugins.ts +29 -28
  86. package/src/cli/lib/plugin-artifact.ts +74 -63
  87. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  88. package/src/cli/lib/plugin-details.ts +27 -42
  89. package/src/cli/lib/plugin-marketplace.ts +6 -8
  90. package/src/cli/lib/plugin-surfaces.ts +8 -7
  91. package/src/cli.ts +1 -3
  92. package/src/config/__tests__/chat-settings.test.ts +121 -0
  93. package/src/config/assistant-feature-flags.ts +28 -23
  94. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  95. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  96. package/src/config/chat-settings.ts +100 -0
  97. package/src/config/default-profile-catalog.ts +1 -1
  98. package/src/config/feature-flag-cache.ts +7 -25
  99. package/src/config/feature-flag-registry.json +9 -9
  100. package/src/config/loader.ts +5 -1
  101. package/src/config/profile-text-generation.ts +25 -21
  102. package/src/config/schemas/conversations.ts +14 -0
  103. package/src/config/schemas/llm.ts +1 -0
  104. package/src/config/schemas/notifications.ts +8 -1
  105. package/src/contacts/gateway-channel-read.ts +1 -1
  106. package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
  107. package/src/conversations/__tests__/auto-archive.test.ts +304 -0
  108. package/src/conversations/auto-archive-activity.ts +61 -0
  109. package/src/conversations/auto-archive.ts +201 -0
  110. package/src/daemon/computer-use-observation.test.ts +81 -0
  111. package/src/daemon/computer-use-observation.ts +23 -0
  112. package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
  113. package/src/daemon/conversation-agent-loop.ts +14 -6
  114. package/src/daemon/conversation-evictor.ts +50 -16
  115. package/src/daemon/conversation-messaging.ts +4 -0
  116. package/src/daemon/conversation-process.ts +15 -0
  117. package/src/daemon/conversation-queue-manager.ts +2 -0
  118. package/src/daemon/conversation.ts +2 -0
  119. package/src/daemon/doordash-steps.ts +1 -3
  120. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
  121. package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
  122. package/src/daemon/handlers/config-a2a.ts +75 -2
  123. package/src/daemon/handlers/shared.ts +13 -2
  124. package/src/daemon/host-cu-proxy.ts +34 -3
  125. package/src/daemon/lifecycle.ts +15 -2
  126. package/src/daemon/process-message.ts +5 -0
  127. package/src/daemon/shutdown-handlers.ts +3 -0
  128. package/src/daemon/turn-finalization.ts +4 -0
  129. package/src/desktop/desktop-browser-endpoint.ts +11 -2
  130. package/src/desktop/desktop-chrome-policy.test.ts +1 -0
  131. package/src/desktop/desktop-chrome-policy.ts +7 -1
  132. package/src/desktop/desktop-computer-use.test.ts +14 -4
  133. package/src/desktop/desktop-session-manager.ts +5 -3
  134. package/src/documents/document-revisions-store.ts +153 -0
  135. package/src/documents/document-store.ts +471 -133
  136. package/src/followups/followup-store.ts +0 -4
  137. package/src/i18n/__tests__/i18n.test.ts +25 -0
  138. package/src/i18n/assistant-text.ts +18 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/messages.ts +11 -0
  141. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  142. package/src/inbound/oauth-callback-url.ts +30 -1
  143. package/src/inbound/public-ingress-urls.ts +12 -2
  144. package/src/ipc/gateway-client.test.ts +40 -0
  145. package/src/ipc/gateway-client.ts +12 -29
  146. package/src/ipc/gateway-flag-listener.ts +7 -10
  147. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
  148. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  149. package/src/live-voice/__tests__/protocol.test.ts +77 -0
  150. package/src/live-voice/__tests__/share-targets.test.ts +163 -0
  151. package/src/live-voice/live-voice-session.ts +23 -6
  152. package/src/live-voice/protocol.ts +22 -0
  153. package/src/live-voice/share-targets.ts +225 -0
  154. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  155. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  156. package/src/mcp/client.ts +1 -19
  157. package/src/mcp/mcp-oauth-provider.ts +66 -49
  158. package/src/media/openrouter-image-service.ts +39 -22
  159. package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
  160. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
  161. package/src/monitoring/control.ts +2 -1
  162. package/src/monitoring/oom-inheritance-sweep.ts +96 -0
  163. package/src/monitoring/oom-kill-reporter.ts +469 -0
  164. package/src/monitoring/resource-sampler.ts +30 -0
  165. package/src/notifications/README.md +53 -4
  166. package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
  167. package/src/notifications/__tests__/broadcaster.test.ts +558 -6
  168. package/src/notifications/__tests__/completion-policy.test.ts +37 -0
  169. package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
  170. package/src/notifications/adapters/platform.ts +14 -0
  171. package/src/notifications/assistant-initiated-thread.ts +101 -0
  172. package/src/notifications/broadcaster.ts +94 -16
  173. package/src/notifications/chat-reply-policy.ts +11 -0
  174. package/src/notifications/completion-policy.ts +12 -2
  175. package/src/notifications/conversation-pairing.ts +15 -70
  176. package/src/notifications/emit-signal.ts +34 -3
  177. package/src/notifications/types.ts +2 -0
  178. package/src/oauth/connect-orchestrator.ts +1 -2
  179. package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
  180. package/src/persistence/conversation-auto-archive.ts +170 -0
  181. package/src/persistence/conversation-crud.ts +2 -1
  182. package/src/persistence/conversation-queries.ts +15 -8
  183. package/src/persistence/embeddings/embed.test.ts +96 -0
  184. package/src/persistence/embeddings/embed.ts +20 -2
  185. package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
  186. package/src/persistence/embeddings/embedding-gemini.ts +39 -1
  187. package/src/persistence/embeddings/embedding-local.ts +6 -0
  188. package/src/persistence/embeddings/qdrant-manager.ts +5 -0
  189. package/src/persistence/job-utils.ts +10 -0
  190. package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
  191. package/src/persistence/migrations/384-add-document-revision.ts +21 -0
  192. package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
  193. package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
  194. package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
  195. package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
  196. package/src/persistence/schema/conversations.ts +1 -0
  197. package/src/persistence/schema/documents.ts +20 -0
  198. package/src/persistence/steps.ts +6 -0
  199. package/src/platform/managed-speech.ts +42 -46
  200. package/src/plugin-api/vision-support.test.ts +21 -21
  201. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
  202. package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
  203. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  204. package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
  205. package/src/providers/anthropic/client.ts +1 -1
  206. package/src/providers/fireworks/client.test.ts +168 -0
  207. package/src/providers/fireworks/client.ts +24 -0
  208. package/src/providers/inference/adapter-factory.ts +6 -0
  209. package/src/providers/ionet/client.test.ts +143 -0
  210. package/src/providers/ionet/client.ts +37 -0
  211. package/src/providers/model-catalog.ts +141 -84
  212. package/src/providers/model-intents.ts +4 -4
  213. package/src/providers/openai/chat-completions-provider.ts +33 -2
  214. package/src/providers/retry.ts +56 -4
  215. package/src/providers/search-provider-catalog.ts +10 -0
  216. package/src/providers/types.ts +10 -2
  217. package/src/providers/unparseable-tool-args.ts +29 -5
  218. package/src/providers/usage-tracking.ts +5 -3
  219. package/src/runtime/assistant-event-hub.ts +23 -0
  220. package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
  221. package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
  222. package/src/runtime/channel-reply-delivery.ts +2 -0
  223. package/src/runtime/channel-retry-sweep.ts +1 -2
  224. package/src/runtime/http-types.ts +3 -0
  225. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  226. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  227. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  228. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  229. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  230. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  231. package/src/runtime/pending-interactions.ts +20 -2
  232. package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
  233. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
  234. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
  235. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
  236. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
  237. package/src/runtime/routes/contact-routes.ts +2 -2
  238. package/src/runtime/routes/conversation-list-routes.ts +10 -2
  239. package/src/runtime/routes/conversation-query-routes.ts +48 -10
  240. package/src/runtime/routes/conversation-routes.ts +7 -0
  241. package/src/runtime/routes/documents-routes.ts +185 -47
  242. package/src/runtime/routes/events-routes.ts +1 -3
  243. package/src/runtime/routes/host-app-control-routes.ts +1 -0
  244. package/src/runtime/routes/host-bash-routes.ts +1 -0
  245. package/src/runtime/routes/host-browser-routes.ts +15 -0
  246. package/src/runtime/routes/host-cu-routes.ts +4 -1
  247. package/src/runtime/routes/host-file-routes.ts +1 -0
  248. package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
  249. package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
  250. package/src/runtime/routes/host-transfer-routes.ts +1 -0
  251. package/src/runtime/routes/identity-routes.ts +3 -3
  252. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  253. package/src/runtime/routes/integrations/a2a.ts +4 -1
  254. package/src/runtime/routes/migration-routes.ts +3 -1
  255. package/src/runtime/routes/notification-routes.ts +1 -2
  256. package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
  257. package/src/runtime/routes/user-route-dispatcher.ts +5 -1
  258. package/src/runtime/verification-templates.ts +1 -2
  259. package/src/sequence/analytics.ts +0 -4
  260. package/src/sequence/engine.ts +1 -11
  261. package/src/sequence/guardrails.ts +1 -6
  262. package/src/sequence/importer.ts +0 -8
  263. package/src/sequence/store.ts +0 -10
  264. package/src/signals/user-message.ts +0 -6
  265. package/src/skills/catalog-install.ts +52 -23
  266. package/src/tools/browser/browser-execution.ts +20 -23
  267. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
  268. package/src/tools/document/document-tool.ts +13 -11
  269. package/src/tools/executor.ts +1 -2
  270. package/src/tools/host-terminal/host-shell.ts +4 -2
  271. package/src/tools/network/__tests__/web-search.test.ts +105 -0
  272. package/src/tools/network/web-fetch.ts +2 -4
  273. package/src/tools/network/web-search.ts +239 -12
  274. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  275. package/src/tools/shared/shell-output.test.ts +42 -0
  276. package/src/tools/shared/shell-output.ts +27 -4
  277. package/src/tools/skills/execute.ts +10 -7
  278. package/src/tools/terminal/shell.ts +3 -3
  279. package/src/tools/tool-approval-handler.ts +9 -1
  280. package/src/tools/ui-surface/visual-validation.ts +1 -2
  281. package/src/usage/attribution.ts +18 -1
  282. package/src/util/__tests__/oom-priority.test.ts +55 -0
  283. package/src/util/__tests__/worker-memory.test.ts +16 -0
  284. package/src/util/debounce.ts +0 -4
  285. package/src/util/oom-priority.ts +79 -0
  286. package/src/util/platform.ts +0 -2
  287. package/src/util/plugin-manifest.ts +5 -7
  288. package/src/util/pricing.ts +25 -0
  289. package/src/util/worker-memory.ts +9 -3
  290. package/src/util/worker-process.ts +8 -0
  291. package/src/workspace/byok-default-profile-ensure.ts +11 -0
  292. package/src/workspace/hatched-date.ts +11 -4
  293. package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
  294. package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
  295. package/src/workspace/migrations/registry.ts +4 -0
@@ -12,6 +12,8 @@ mock.module("../../inbound/platform-callback-registration.js", () => ({
12
12
  mock.module("../../inbound/public-ingress-urls.js", () => ({
13
13
  getOAuthCallbackUrl: () =>
14
14
  "https://platform.example/v1/gateway/callbacks/abc/webhooks/oauth/callback/",
15
+ getMcpOAuthClientMetadataUrl: () =>
16
+ "http://localhost/oauth/client-metadata.json",
15
17
  }));
16
18
 
17
19
  mock.module("../../config/loader.js", () => ({
package/src/mcp/client.ts CHANGED
@@ -22,24 +22,6 @@ const log = getLogger("mcp-client");
22
22
 
23
23
  const CONNECT_TIMEOUT_MS = 30_000;
24
24
 
25
- // MCP servers occasionally return tools with a missing or non-object
26
- // `inputSchema` (spec violation, but seen in the wild). Coerce to a valid
27
- // empty object schema so downstream code that assumes `input_schema: object`
28
- // (e.g. `injectActivityField`) doesn't crash.
29
- function normalizeInputSchema(
30
- raw: unknown,
31
- toolName: string,
32
- ): Record<string, unknown> {
33
- if (raw != null && typeof raw === "object" && !Array.isArray(raw)) {
34
- return raw as Record<string, unknown>;
35
- }
36
- log.warn(
37
- { toolName, received: typeof raw },
38
- "MCP tool returned non-object inputSchema; defaulting to empty object schema",
39
- );
40
- return { type: "object", properties: {} };
41
- }
42
-
43
25
  /**
44
26
  * Behavioral hints a server may attach to a tool (MCP `ToolAnnotations`).
45
27
  * Self-reported by the server, so consumers treat them as hints rather than
@@ -258,7 +240,7 @@ export class McpClient {
258
240
  return result.tools.map((tool) => ({
259
241
  name: tool.name,
260
242
  description: tool.description ?? "",
261
- inputSchema: normalizeInputSchema(tool.inputSchema, tool.name),
243
+ inputSchema: tool.inputSchema,
262
244
  annotations: tool.annotations,
263
245
  }));
264
246
  }
@@ -27,6 +27,7 @@ import type {
27
27
  OAuthClientMetadata,
28
28
  OAuthTokens,
29
29
  } from "@modelcontextprotocol/sdk/shared/auth.js";
30
+ import { buildMcpOAuthClientMetadata } from "@vellumai/service-contracts/mcp-oauth";
30
31
 
31
32
  import { getPlatformAssistantId } from "../config/env.js";
32
33
  import { getAssistantName } from "../daemon/identity-helpers.js";
@@ -49,26 +50,16 @@ import {
49
50
  const log = getLogger("mcp-oauth");
50
51
 
51
52
  /**
52
- * Logo shown on an authorization server's consent screen.
53
+ * What stored client information was made against.
53
54
  *
54
- * Anonymously fetchable, which is the requirement: the server loads it
55
- * without credentials. It identifies Vellum rather than the individual
56
- * assistant, so every assistant a person runs presents the same mark.
57
- */
58
- const CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
59
-
60
- /**
61
- * What a stored client registration was made against.
62
- *
63
- * A client identifier belongs to the authorization server that issued it and
64
- * is registered for one set of redirect URIs, so reusing a registration after
65
- * either changes is invalid. Recording both is what lets a registration be
66
- * reused when they still hold, which is the difference between registering
67
- * once per assistant and registering once per attempt.
55
+ * A client identifier applies to one authorization server and one set of
56
+ * redirect URIs, so reusing it after either changes is invalid. Recording both
57
+ * avoids selecting the client again on every connection attempt.
68
58
  */
69
59
  interface ClientRegistrationBinding {
70
60
  issuer: string | null;
71
61
  redirectUri: string;
62
+ clientMetadataUrl?: string;
72
63
  }
73
64
 
74
65
  export interface McpOAuthCallbackResult {
@@ -96,6 +87,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
96
87
  private _codeVerifier: string | undefined;
97
88
  private _state: string | undefined;
98
89
  private _redirectUrl: string | undefined;
90
+ private _clientMetadataUrl: string | undefined;
99
91
  private _codePromise: Promise<string> | null = null;
100
92
  /** Deferred resolver/rejector for the callback code promise. */
101
93
  private _codeResolve: ((code: string) => void) | undefined;
@@ -127,6 +119,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
127
119
  return this._redirectUrl;
128
120
  }
129
121
 
122
+ get clientMetadataUrl(): string | undefined {
123
+ return this._clientMetadataUrl;
124
+ }
125
+
130
126
  // --- clientMetadata ---
131
127
 
132
128
  /**
@@ -143,23 +139,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
143
139
  * `logo_uri` is the Vellum mark rather than the assistant's own avatar.
144
140
  * An authorization server fetching a logo is anonymous, and the avatar is
145
141
  * served only behind authentication, so there is no per-assistant URL to
146
- * give it. A stable public identity per assistant is what would supply
147
- * one, and the same prerequisite would let this move to Client ID
148
- * Metadata Documents and drop registration altogether.
142
+ * give it.
149
143
  */
150
144
  get clientMetadata(): OAuthClientMetadata {
151
145
  const assistantName = getAssistantName();
152
146
  const assistantId = getPlatformAssistantId().trim();
153
- return {
154
- client_name: assistantName ?? "Vellum Assistant",
155
- logo_uri: CLIENT_LOGO_URI,
156
- redirect_uris: this._redirectUrl ? [this._redirectUrl] : [],
157
- token_endpoint_auth_method: "none",
158
- grant_types: ["authorization_code", "refresh_token"],
159
- response_types: ["code"],
160
- ...(assistantId.length > 0 && { software_id: assistantId }),
161
- software_version: APP_VERSION,
162
- };
147
+ return buildMcpOAuthClientMetadata({
148
+ clientName: assistantName ?? "Vellum Assistant",
149
+ redirectUris: this._redirectUrl ? [this._redirectUrl] : [],
150
+ softwareId: assistantId.length > 0 ? assistantId : undefined,
151
+ softwareVersion: APP_VERSION,
152
+ });
163
153
  }
164
154
 
165
155
  // --- Tokens ---
@@ -254,15 +244,12 @@ export class McpOAuthProvider implements OAuthClientProvider {
254
244
  // --- Client Information ---
255
245
 
256
246
  /**
257
- * The stored registration, or `undefined` to make the SDK register a new
258
- * one.
247
+ * The stored client information, or `undefined` to make the SDK select a
248
+ * client through CIMD or DCR.
259
249
  *
260
- * Returning the stored value is the normal case: dynamic registration
261
- * writes a record on the authorization server, so re-registering per
262
- * attempt accumulates records nobody cleans up. It is withheld only when
263
- * the registration provably no longer applies, because the redirect URI
264
- * it was made for changed or because a different authorization server now
265
- * fronts the resource.
250
+ * Returning the stored value is the normal case. It is withheld only when
251
+ * the client information provably no longer applies because its redirect
252
+ * URI, metadata URL, or authorization server changed.
266
253
  */
267
254
  async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
268
255
  const raw = await getSecureKeyAsync(
@@ -287,7 +274,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
287
274
  if (stale) {
288
275
  log.info(
289
276
  { serverId: this.serverId, reason: stale },
290
- "Stored client registration no longer applies; registering again",
277
+ "Stored OAuth client information no longer applies; selecting a client again",
291
278
  );
292
279
  return undefined;
293
280
  }
@@ -309,12 +296,16 @@ export class McpOAuthProvider implements OAuthClientProvider {
309
296
  return;
310
297
  }
311
298
 
312
- // Record what the registration was made against, so a later run can tell
299
+ // Record what the client information applies to, so a later run can tell
313
300
  // whether reusing it is still valid.
314
301
  if (this._redirectUrl) {
315
302
  const binding: ClientRegistrationBinding = {
316
303
  issuer: await this.currentIssuer(),
317
304
  redirectUri: this._redirectUrl,
305
+ ...(this._clientMetadataUrl &&
306
+ info.client_id === this._clientMetadataUrl && {
307
+ clientMetadataUrl: this._clientMetadataUrl,
308
+ }),
318
309
  };
319
310
  const boundOk = await setSecureKeyAsync(
320
311
  mcpOAuthCredentialKey(this.credentialTarget, "client_binding"),
@@ -323,7 +314,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
323
314
  if (!boundOk) {
324
315
  log.warn(
325
316
  { serverId: this.serverId },
326
- "Failed to persist OAuth client binding; the registration will be remade on the next flow",
317
+ "Failed to persist OAuth client binding; the client will be selected again on the next flow",
327
318
  );
328
319
  }
329
320
  }
@@ -342,14 +333,14 @@ export class McpOAuthProvider implements OAuthClientProvider {
342
333
  }
343
334
 
344
335
  /**
345
- * Why the stored registration cannot be reused, or null when it can.
336
+ * Why the stored client information cannot be reused, or null when it can.
346
337
  *
347
338
  * The redirect URI is only checked while a flow is being prepared:
348
339
  * outside one there is no redirect URI to compare against, and a silent
349
- * reconnect must not be turned into a registration.
340
+ * reconnect must not trigger client selection.
350
341
  *
351
342
  * The issuer is only checked when discovery has run. An unverifiable
352
- * issuer keeps the registration rather than discarding it, since the
343
+ * issuer keeps the client information rather than discarding it, since the
353
344
  * redirect check already covers the case this plugin actually changes.
354
345
  */
355
346
  private async describeStaleBinding(): Promise<string | null> {
@@ -357,8 +348,8 @@ export class McpOAuthProvider implements OAuthClientProvider {
357
348
  mcpOAuthCredentialKey(this.credentialTarget, "client_binding"),
358
349
  );
359
350
  if (!raw) {
360
- // Registered before the binding was recorded. Keep it: discarding
361
- // every pre-existing registration would remake all of them at once.
351
+ // Saved before the binding was recorded. Keep it: discarding every
352
+ // pre-existing client would recreate all of them at once.
362
353
  return null;
363
354
  }
364
355
 
@@ -373,6 +364,25 @@ export class McpOAuthProvider implements OAuthClientProvider {
373
364
  return "redirect URI changed";
374
365
  }
375
366
 
367
+ if (
368
+ this._redirectUrl &&
369
+ binding.clientMetadataUrl &&
370
+ binding.clientMetadataUrl !== this._clientMetadataUrl
371
+ ) {
372
+ return "client metadata URL changed";
373
+ }
374
+
375
+ if (this._redirectUrl && binding.clientMetadataUrl) {
376
+ const discovery = await this.discoveryState();
377
+ if (
378
+ discovery &&
379
+ discovery.authorizationServerMetadata
380
+ ?.client_id_metadata_document_supported !== true
381
+ ) {
382
+ return "authorization server no longer supports client metadata documents";
383
+ }
384
+ }
385
+
376
386
  const issuer = await this.currentIssuer();
377
387
  if (issuer && binding.issuer && binding.issuer !== issuer) {
378
388
  return "authorization server changed";
@@ -536,9 +546,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
536
546
  "OAuth client information key not found in secure storage (already removed)",
537
547
  );
538
548
  }
539
- // The binding describes the registration being dropped, so it goes
540
- // with it. Leaving it would let a later registration inherit the
541
- // previous one's issuer and redirect URI.
549
+ // The binding describes the client information being dropped, so it
550
+ // goes with it. Leaving it would let a later client inherit the previous
551
+ // one's issuer and redirect URI.
542
552
  await deleteSecureKeyAsync(
543
553
  mcpOAuthCredentialKey(this.credentialTarget, "client_binding"),
544
554
  );
@@ -580,10 +590,13 @@ export class McpOAuthProvider implements OAuthClientProvider {
580
590
  * the first point at which the SDK-generated `state` is known.
581
591
  */
582
592
  async startCallbackServer(): Promise<McpOAuthCallbackResult> {
583
- const { resolveOauthCallbackUrl } =
593
+ const { resolveMcpOAuthClientMetadataUrl, resolveOauthCallbackUrl } =
584
594
  await import("../inbound/oauth-callback-url.js");
585
595
 
586
596
  this._redirectUrl = await resolveOauthCallbackUrl();
597
+ this._clientMetadataUrl = resolveMcpOAuthClientMetadataUrl(
598
+ this._redirectUrl,
599
+ );
587
600
 
588
601
  const codePromise = new Promise<string>((resolve, reject) => {
589
602
  this._codeResolve = resolve;
@@ -598,7 +611,11 @@ export class McpOAuthProvider implements OAuthClientProvider {
598
611
  void codePromise.catch(() => {});
599
612
 
600
613
  log.info(
601
- { serverId: this.serverId, redirectUrl: this._redirectUrl },
614
+ {
615
+ serverId: this.serverId,
616
+ redirectUrl: this._redirectUrl,
617
+ clientMetadataUrl: this._clientMetadataUrl,
618
+ },
602
619
  "MCP OAuth callback prepared (awaiting state from auth URL)",
603
620
  );
604
621
 
@@ -1,3 +1,5 @@
1
+ import { z } from "zod";
2
+
1
3
  import {
2
4
  DEFAULT_OPENROUTER_IMAGE_MODEL,
3
5
  qualifyImageModelForOpenRouter,
@@ -13,6 +15,31 @@ import {
13
15
  } from "./types.js";
14
16
 
15
17
  const OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images";
18
+ const OpenRouterImageResponseSchema = z.object({
19
+ data: z
20
+ .array(
21
+ z
22
+ .object({
23
+ b64_json: z.string().optional().catch(undefined),
24
+ media_type: z.string().optional().catch(undefined),
25
+ })
26
+ .optional()
27
+ .catch(undefined),
28
+ )
29
+ .optional()
30
+ .catch(undefined),
31
+ error: z
32
+ .union([
33
+ z.string(),
34
+ z.object({
35
+ message: z.string().optional().catch(undefined),
36
+ }),
37
+ ])
38
+ .optional()
39
+ .catch(undefined),
40
+ });
41
+
42
+ type OpenRouterImageResponse = z.infer<typeof OpenRouterImageResponseSchema>;
16
43
 
17
44
  /**
18
45
  * Qualify a bare built-in ID or alias for OpenRouter. Call sites that skip
@@ -76,21 +103,11 @@ export function mapOpenRouterError(error: unknown): string {
76
103
  return "An unexpected error occurred during image generation.";
77
104
  }
78
105
 
79
- function errorMessage(body: unknown): string {
80
- if (typeof body !== "object" || body === null) {
81
- return "Request failed";
82
- }
83
- const error = (body as { error?: unknown }).error;
84
- if (typeof error === "string") {
85
- return error;
86
- }
87
- if (typeof error === "object" && error !== null) {
88
- const message = (error as { message?: unknown }).message;
89
- if (typeof message === "string" && message) {
90
- return message;
91
- }
106
+ function errorMessage(body: OpenRouterImageResponse | undefined): string {
107
+ if (typeof body?.error === "string") {
108
+ return body.error;
92
109
  }
93
- return "Request failed";
110
+ return body?.error?.message || "Request failed";
94
111
  }
95
112
 
96
113
  export async function generateImageOpenRouter(
@@ -98,7 +115,9 @@ export async function generateImageOpenRouter(
98
115
  request: ImageGenerationRequest,
99
116
  ): Promise<ImageGenerationResult> {
100
117
  if (credentials.type !== "direct") {
101
- throw new Error("OpenRouter image generation requires an OpenRouter API key.");
118
+ throw new Error(
119
+ "OpenRouter image generation requires an OpenRouter API key.",
120
+ );
102
121
  }
103
122
 
104
123
  const model = resolveOpenRouterModel(request.model);
@@ -127,12 +146,10 @@ export async function generateImageOpenRouter(
127
146
  ...(request.signal ? { signal: request.signal } : {}),
128
147
  });
129
148
 
130
- const body = (await response.json().catch(() => undefined)) as
131
- | {
132
- data?: Array<{ b64_json?: string; media_type?: string }>;
133
- error?: unknown;
134
- }
135
- | undefined;
149
+ const parsed = OpenRouterImageResponseSchema.safeParse(
150
+ await response.json().catch(() => undefined),
151
+ );
152
+ const body = parsed.success ? parsed.data : undefined;
136
153
 
137
154
  if (!response.ok) {
138
155
  throw new OpenRouterImageError(response.status, errorMessage(body));
@@ -140,7 +157,7 @@ export async function generateImageOpenRouter(
140
157
 
141
158
  const images: GeneratedImage[] = [];
142
159
  for (const entry of body?.data ?? []) {
143
- if (!entry.b64_json) {
160
+ if (!entry?.b64_json) {
144
161
  continue;
145
162
  }
146
163
  images.push({
@@ -0,0 +1,86 @@
1
+ import {
2
+ mkdirSync,
3
+ mkdtempSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { sweepInheritedOomProtection } from "../oom-inheritance-sweep.js";
13
+ import { stat } from "./proc-fixtures.js";
14
+
15
+ const DAEMON = 100;
16
+ const MONITOR = 200;
17
+
18
+ let procRoot: string;
19
+
20
+ function writeProcess(
21
+ pid: number,
22
+ ppid: number,
23
+ comm: string,
24
+ oomScoreAdj: number | null,
25
+ ): void {
26
+ const dir = join(procRoot, String(pid));
27
+ mkdirSync(dir, { recursive: true });
28
+ writeFileSync(join(dir, "stat"), stat(pid, comm, "S", ppid, 100));
29
+ if (oomScoreAdj != null) {
30
+ writeFileSync(join(dir, "oom_score_adj"), `${oomScoreAdj}\n`);
31
+ }
32
+ }
33
+
34
+ function readAdj(pid: number): string {
35
+ return readFileSync(join(procRoot, String(pid), "oom_score_adj"), "utf-8");
36
+ }
37
+
38
+ beforeEach(() => {
39
+ procRoot = mkdtempSync(join(tmpdir(), "oom-sweep-"));
40
+ writeProcess(1, 0, "init", 0);
41
+ writeProcess(DAEMON, 1, "bun", -700);
42
+ writeProcess(MONITOR, DAEMON, "bun", -500);
43
+ });
44
+
45
+ afterEach(() => {
46
+ rmSync(procRoot, { recursive: true, force: true });
47
+ });
48
+
49
+ describe("sweepInheritedOomProtection", () => {
50
+ test("resets negative descendants and leaves everything else alone", () => {
51
+ writeProcess(300, DAEMON, "node", -700); // forgot to reset
52
+ writeProcess(400, 300, "python3", -700); // inherited through 300
53
+ writeProcess(500, 1, "sshd", -700); // not under the daemon
54
+ writeProcess(600, DAEMON, "bash", 1000); // already raised
55
+ writeProcess(700, DAEMON, "zombie", null); // no oom_score_adj file
56
+
57
+ const resets = sweepInheritedOomProtection({
58
+ daemonPid: DAEMON,
59
+ keepPids: new Set([MONITOR]),
60
+ procRoot,
61
+ });
62
+
63
+ expect(resets.sort((a, b) => a.pid - b.pid)).toEqual([
64
+ { pid: 300, comm: "node", previous: -700 },
65
+ { pid: 400, comm: "python3", previous: -700 },
66
+ ]);
67
+ expect(readAdj(300)).toBe("0");
68
+ expect(readAdj(400)).toBe("0");
69
+ expect(readAdj(DAEMON)).toBe("-700\n");
70
+ expect(readAdj(MONITOR)).toBe("-500\n");
71
+ expect(readAdj(500)).toBe("-700\n");
72
+ expect(readAdj(600)).toBe("1000\n");
73
+ });
74
+
75
+ test("tolerates a ppid cycle in a malformed table", () => {
76
+ writeProcess(300, 400, "a", -700);
77
+ writeProcess(400, 300, "b", -700);
78
+ expect(
79
+ sweepInheritedOomProtection({
80
+ daemonPid: DAEMON,
81
+ keepPids: new Set([MONITOR]),
82
+ procRoot,
83
+ }),
84
+ ).toEqual([]);
85
+ });
86
+ });