@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
package/.env.example CHANGED
@@ -2,6 +2,8 @@
2
2
  ANTHROPIC_API_KEY=sk-ant-...
3
3
  OPENAI_API_KEY=sk-...
4
4
  GEMINI_API_KEY=...
5
+ # Optional. IO Intelligence (io.net) open-models endpoint.
6
+ IONET_API_KEY=...
5
7
  # Optional. TypeSafe / Jev System One decision model.
6
8
  TYPESAFE_API_KEY=...
7
9
  # Optional for authenticated/proxied Ollama deployments
package/ARCHITECTURE.md CHANGED
@@ -45,6 +45,12 @@ Resource pressure monitoring reports sustained CPU/memory pressure on platform-h
45
45
 
46
46
  **Lifecycle:** `src/daemon/resource-pressure-guard-lifecycle.ts` starts the guard at daemon boot with the first sample deferred onto a macrotask so it never blocks startup, and stops the guard (cancelling any pending deferred sample) on shutdown. The web chat banner built on this status is documented in the repo-level [`/ARCHITECTURE.md`](../ARCHITECTURE.md) "Resource Pressure Monitoring" section.
47
47
 
48
+ ### OOM Priority
49
+
50
+ When the container hits its memory limit, the kernel kills the process with the highest `oom_score` (memory share plus `oom_score_adj`). `src/util/oom-priority.ts` steers that choice so the assistant is never the victim of a runaway tool: the daemon sets itself to -700 as the first thing in `runDaemon`, before any spawn. Children inherit the parent's value at fork, so every spawn path resets its child: shell children get +1000 from the Linux prefix in `buildShellInvocation` (`packages/environments`), desktop children and ACP agents and terminals get +1000 from `withOomScoreAdj`, an `sh -c ... exec` wrapper that keeps pid and process group, and workers, the embedding worker, and Qdrant get 0 from a parent-side write right after spawn (the resource monitor gets -500 so it outlives everything but the daemon and records the kill). The monitor also sweeps the daemon's descendants every few seconds (`src/monitoring/oom-inheritance-sweep.ts`) and writes 0 over any negative value it finds, logging the comm, so a spawn path that forgets the reset degrades to a few seconds of inherited protection instead of a protected runaway. Bundled plugins (the memory plugin's rerank worker, for one) rely on the sweep rather than importing the helper, since the plugin import boundary forbids reaching outside the plugin. Raising never needs a capability; lowering needs `CAP_SYS_RESOURCE`, which the platform grants and self-hosted Docker does not, where the daemon simply stays at 0. Nothing here caps a single process or acts early: it only decides who dies once the kernel has to kill.
51
+
52
+ **Verification telemetry:** the resource monitor reports kernel OOM kills in the container as a `watchdog` event with `check_name` `oom_kill` (`src/monitoring/oom-kill-reporter.ts`). It reads the kernel log through `dmesg` (the `/dev/kmsg` device is not mounted in the container; `dmesg` uses the syslog syscall, which the Kata profile permits) when the cgroup `memory.events` kill counter moves, on its first scan after start (the kill that took the daemon down restarts the monitor with it), and once a minute as a fallback. The log is shared by every container on that kernel, so a kill is attributed by the cgroup path in the kernel's `oom-kill:` header, matched against the container's own id, which it reads from the runtime-named mounts in `/proc/self/mountinfo` because `/proc/self/cgroup` reads `/` inside a cgroup namespace (`attribution: "cgroup"`). The kernel rate-limits that header in a burst, so header-less kills are attributed by the counter, re-read after the log: credit left after the header-attributed kills names header-less kills oldest first, header-less kills beyond the credit are another container's, and credit for a kill counted but not yet logged is spent on the next scan or reported unnamed (`attribution: "cgroup_counter"`). The first scan and hosts without the counter claim every new kill not assigned elsewhere by a header (`attribution: "kernel_log"`). The `detail` bag lists each victim's comm, `oom_score_adj` and resident size, the cgroup memory figures at the time, and `killed_daemon`, detected by the daemon's -700 score rather than a pid the restart has since replaced; where the log is unreadable a counter move still yields a victimless event. A cursor (kernel boot id plus last log timestamp) on the monitor data dir advances only after the event is queued, so a telemetry store outage delays a report rather than losing one, and resets when the kernel reboots.
53
+
48
54
  ### Single-Header JWT Auth Model
49
55
 
50
56
  All HTTP API requests use a single `Authorization: Bearer <jwt>` header for authentication. The JWT carries identity, permissions, and policy versioning in a unified token.
@@ -612,12 +618,12 @@ Every phone call connects over Twilio Media Streams: the voice webhook emits `<C
612
618
 
613
619
  Transcription mode is selected once per session in `media-stream-stt-session.ts`:
614
620
 
615
- - **Streaming** (default): when `calls.voice.telephonyStreaming` is enabled and the `telephony` role resolves a streaming transcriber (`resolveStreamingTranscriber({ role: "telephony" })`), inbound audio is decoded (mu-law → PCM16, resampled 8 kHz → 16 kHz) and fed to the provider's realtime adapter. Replies trigger only on utterance-boundary finals (for Deepgram, `speech_final`/`UtteranceEnd`, never mid-sentence `is_final` segments), and barge-in fires from local energy VAD, never from transcriber partials. While something is interruptible (an assistant turn in flight, thinking or speaking, or a completed turn's tail still playing from Twilio's buffer), caller speech arms the shared sustained-speech barge-in guard (`src/calls/barge-in-guard.ts`, the same accounting live voice uses: speech accumulates toward 250 ms, short gaps are tolerated, a run that is mostly silence resets) and every inbound frame feeds it; outside that window the guard is dropped, so the caller's own utterance never carries into a turn that starts before the local VAD ends it. Only a fired guard reaches `CallController.handleBargeIn`, which interrupts a turn in either phase and ignores an idle controller (the playing tail is cleared instead).
621
+ - **Streaming** (default): when `calls.voice.telephonyStreaming` is enabled and the `telephony` role resolves a streaming transcriber (`resolveStreamingTranscriber({ role: "telephony" })`), inbound audio is decoded (mu-law → PCM16, resampled 8 kHz → 16 kHz) and fed to the provider's realtime adapter. Replies trigger only on utterance-boundary finals (for Deepgram, `speech_final`/`UtteranceEnd`, never mid-sentence `is_final` segments), and barge-in fires from local energy VAD, never from transcriber partials. Interim `partial` events are forwarded to the controller as `handleCallerPartial`: they commit nothing and start no turn, they are never persisted and never reach a setup flow, and they exist so a boundary the front door held mid-thought can be replayed on the caller's newest words rather than only the ones the provider has finalized. While something is interruptible (an assistant turn in flight, thinking or speaking, or a completed turn's tail still playing from Twilio's buffer), caller speech arms the shared sustained-speech barge-in guard (`src/calls/barge-in-guard.ts`, the same accounting live voice uses: speech accumulates toward 250 ms, short gaps are tolerated, a run that is mostly silence resets) and every inbound frame feeds it; outside that window the guard is dropped, so the caller's own utterance never carries into a turn that starts before the local VAD ends it. Only a fired guard reaches `CallController.handleBargeIn`, which interrupts a turn in either phase and ignores an idle controller (the playing tail is cleared instead).
616
622
  - **Batch fallback**: otherwise the session segments turns with the energy-based `MediaTurnDetector` and transcribes each completed turn via the same role's batch API. Both halves of a call read the `telephony` role, which is why a role names its consumer rather than a boundary.
617
623
 
618
- Every phone turn runs the same two-leg triage as live voice through `startVoiceTurn` (`src/calls/voice-session-bridge.ts`): `call-controller.ts` opens on a toolless front-door leg (`routingLeg: "front-door"`, the `voiceFrontDoor` call site) and drives it through the shared `createFrontDoorLegCoordinator` (`src/calls/voice-leg-coordinator.ts`), which reads the stream through the verdict machine and sequences the hand-off (pause narration, abort the leg, resolve and speak the bridge, mark it as the floor holder, start the escalated leg pinned to the conversation's own model, re-arm narration); each driver supplies only a host for how text and the bridge are spoken, how a leg is started or aborted, and (live voice only) the speculative hold and commit. Phone has no partial transcripts, so the hold verdict is never taught and routing is escalate-only. The controller also passes the bridge's turn callbacks (tool activity is recorded as `tool_use_started` / `tool_use_completed` call events, persisted row ids ride the `assistant_spoke` event), `launchedAtMs` for dispatch timing, and a `voiceTelemetry` bag keyed by the call session with a `phone_inbound` / `phone_outbound` entry. Both drivers share the spoken progress narration cadence (`src/calls/voice-progress-cadence.ts`, tuned by `voice.frontModel.progress`): the cadence owns the tool-activity log, the triggers (an ops burst, a long operation completing, a full interval of audible silence with news, the `maxSilenceMs` heartbeat) and the generated or static phrase, while each driver supplies its own view of audible silence (live voice from its TTS queue and playback-tail estimate; the media-stream transport from `isPlaybackIdle()` and a running sum of sent frame durations) and how to speak a phrase.
624
+ Every phone turn runs the same two-leg triage as live voice through `startVoiceTurn` (`src/calls/voice-session-bridge.ts`): `call-controller.ts` opens on a toolless front-door leg (`routingLeg: "front-door"`, the `voiceFrontDoor` call site) and drives it through the shared `createFrontDoorLegCoordinator` (`src/calls/voice-leg-coordinator.ts`), which reads the stream through the verdict machine and sequences the hand-off (pause narration, abort the leg, resolve and speak the bridge, mark it as the floor holder, start the escalated leg pinned to the conversation's own model, re-arm narration); each driver supplies only a host for how text and the bridge are spoken, how a leg is started or aborted, and (live voice only) the speculative hold and commit. A leg that carries the caller's own words is also the turn's endpoint decision, so it is taught the hold verdict: silence alone cannot tell a finished sentence from a thinking pause, and answering a fragment makes the rest of the sentence read as a barge-in that interrupts the assistant's own reply. On a hold the leg is discarded (its persisted user row rolled back through the handle's `discard`), the floor goes straight back to the caller, and the words are carried into their next final so the whole sentence is judged together. If no next words arrive, a replay timer (`voice.frontModel.endpointExtensionMs`) answers the sentence as it stands; any partial re-arms that timer and the next final cancels it. A partial also stretches the wait to at least `calls.voice.utteranceEndMs` plus a margin, because the provider still owes a final for those words and that final is the better boundary: racing it would ask the same words twice, once on the interim guess and once on the committed text. Reaching the timer with a partial outstanding therefore means no final is coming, so the interim text rides along as the only record of what the caller said. `voice.frontModel.endpointMaxExtensions` caps consecutive holds per sentence, after which the leg must answer. Openers, instructions, and other assistant-initiated turns are never dispatched that way. The controller also passes the bridge's turn callbacks (tool activity is recorded as `tool_use_started` / `tool_use_completed` call events, persisted row ids ride the `assistant_spoke` event), `launchedAtMs` for dispatch timing, and a `voiceTelemetry` bag keyed by the call session with a `phone_inbound` / `phone_outbound` entry. Both drivers share the spoken progress narration cadence (`src/calls/voice-progress-cadence.ts`, tuned by `voice.frontModel.progress`): the cadence owns the tool-activity log, the triggers (an ops burst, a long operation completing, a full interval of audible silence with news, the `maxSilenceMs` heartbeat) and the generated or static phrase, while each driver supplies its own view of audible silence (live voice from its TTS queue and playback-tail estimate; the media-stream transport from `isPlaybackIdle()` and a running sum of sent frame durations) and how to speak a phrase.
619
625
 
620
- Both drivers also share the per-turn latency marks (`src/calls/voice-metrics.ts`). The controller opens a turn's marks when it dispatches, seeded with when the caller's transcript arrived, and stamps the leg dispatch, the first assistant delta, the first audible audio, a barge-in, and how the turn settled. Live voice streams a snapshot to its client on every mark; a call has no client on the line, so the aggregate is logged once per turn, cancelled turns included. Each call also records a `phone_call_started` / `phone_call_ended` pair on the onboarding telemetry substrate (`src/telemetry/phone-call-funnel.ts`), written by the call store at session creation and at the single terminal status transition: duration is the gap between the two rows, and a call that took no caller turn carries why on the end row (`silent_no_connect` when it never connected, `silent_no_turn` when it did and nobody spoke).
626
+ Both drivers also share the per-turn latency marks (`src/calls/voice-metrics.ts`). The controller opens a turn's marks when it dispatches, seeded with when the caller's transcript arrived and when the provider first had interim words for that same utterance, and stamps the leg dispatch, the first assistant delta, the first audible audio, a barge-in, an endpoint hold, and how the turn settled. A held turn settles as `endpoint_hold` and hands both seeds to the turn that finally answers, so the caller's wait is still measured from the moment they stopped talking. Live voice streams a snapshot to its client on every mark; a call has no client on the line, so the aggregate is logged once per turn, cancelled turns included. Each call also records a `phone_call_started` / `phone_call_ended` pair on the onboarding telemetry substrate (`src/telemetry/phone-call-funnel.ts`), written by the call store at session creation and at the single terminal status transition: duration is the gap between the two rows, and a call that took no caller turn carries why on the end row (`silent_no_connect` when it never connected, `silent_no_turn` when it did and nobody spoke).
621
627
 
622
628
  A credential preflight (`resolveTelephonyCredentialReadiness()` in `src/calls/telephony-credential-preflight.ts`) gates every call: it requires a credentialed, telephony-capable STT provider **and** a media-stream-playable TTS provider (the configured one or a credentialed playable fallback). Inbound calls that fail the preflight receive `<Say>` setup-required copy plus `<Hangup/>` instead of a doomed stream; outbound placement fails before dialing via `preflightVoiceIngress()` with the same user-facing message.
623
629
 
@@ -902,6 +908,23 @@ graph LR
902
908
 
903
909
  ---
904
910
 
911
+ ## Automatic Done
912
+
913
+ `src/conversations/auto-archive.ts` runs an opt-in automatic Done sweep after startup recovery, on assistant config or feature-flag invalidation, and hourly. It marks eligible inactive native chats through the existing `archived_at` state. Candidate selection and the conditional update share the same persisted eligibility predicates; runtime checks run synchronously with each conditional write. Explicit reopening records `last_reopened_at` to grant a full inactivity interval.
914
+
915
+ ```mermaid
916
+ flowchart LR
917
+ TRIGGER[Startup recovery / config or feature-flag change / hourly timer] --> PAGE[Bounded candidate page]
918
+ PAGE --> GUARDIAN[Read pending guardian requests]
919
+ GUARDIAN --> CHECK[Recheck config and runtime activity]
920
+ CHECK --> CAS[Conditional SQLite Done update]
921
+ CAS --> SYNC[Conversation list and metadata invalidation]
922
+ ```
923
+
924
+ Readiness, eligibility, restart behavior, and the gateway snapshot boundary are documented in [Automatic Done](docs/automatic-done.md).
925
+
926
+ ---
927
+
905
928
  ## Plugin-Declared Schedules: Declaration → Reconciler → cron_jobs
906
929
 
907
930
  Plugins contribute recurring schedules as a surface: directories under
@@ -0,0 +1,32 @@
1
+ # Automatic Done
2
+
3
+ Chats Settings exposes `conversations.autoArchive` through the assistant config. `enabled` defaults to `false`; `afterDays` defaults to `7` and accepts `1`, `7`, `14`, or `30`. The worker additionally requires the `sidebar-done` feature flag so the setting cannot activate legacy archive behavior.
4
+
5
+ Auto Archive marks eligible inactive chats **Done** by setting the existing `archived_at` field. The chat remains visible in All Chats and Done, retains its transcript, and can be reopened. No schedule is cancelled and no data is deleted. New visible activity uses the existing resurfacing path.
6
+
7
+ ## Eligibility
8
+
9
+ Inactivity is elapsed wall time since the latest of the last message (or creation for an empty chat) and an explicit reopen. Title edits, read receipts, config edits and other `updated_at` writes do not count as conversation activity. Migration `386-conversations-last-reopened-at.ts` adds a nullable marker without backfilling or changing existing chats. Only an actual Done-to-open transition records that marker, giving a reopened chat a full configured interval.
10
+
11
+ Candidates must be unarchived standard native chats. Native notification-origin chats are eligible; an external channel binding takes precedence over the legacy origin field. Pinned, private, scheduled, background, system, consolidation and child/subagent conversations are excluded. Chats with unread assistant output, pending guardian input, a live ACP session, a running workflow, an active mode session or a nonterminal subagent are excluded.
12
+
13
+ The final runtime check also excludes accepted sends, queued/processing turns, voice residency, pending standalone surfaces, local interactions, background tool work, wake dispatches and unfinished turn finalization. ACP resume reservations are checked against their persisted parent before the resumed session registers as live.
14
+
15
+ ## Lifecycle and concurrency
16
+
17
+ The assistant owns one worker, independent of client pages. Its first sweep waits for interrupted conversation recovery, database migration readiness and startup completion. Failed startup recovery leaves the worker paused until restart. Config or assistant feature-flag invalidation and an hourly timer trigger later sweeps; overlapping triggers coalesce. The gateway flag listener publishes invalidation only after its cache refresh succeeds, including reconnect catch-up, so consumers see the refreshed gate. A failed refresh emits no invalidation. Shutdown cancels retries, removes the subscription/timer and prevents a late recovery promise from starting work.
18
+
19
+ Each sweep scans bounded pages of 100 candidates with an activity/id keyset. For every page, it reads the exhaustive pending guardian-request list through the gateway. A failed read leaves that page untouched. Final eligibility and the write run synchronously inside each SQLite retry attempt. The SQL compare-and-set checks sampled message/reopen cursors and all persisted eligibility again, so activity, pinning, input-related processing, workflow/session changes, or reopening during the asynchronous gateway read cannot overwrite newer state.
20
+
21
+ Gateway-owned guardian requests live outside the assistant database. The page read is a snapshot, not a distributed transaction: a remote request created after that read cannot be atomically excluded by SQLite. Local interaction, processing and queue checks close the ordinary same-process window. The worker fails closed on an unavailable gateway or uncertain runtime state; the cross-service snapshot boundary remains explicit.
22
+
23
+ Successful updates publish the existing conversation-list and per-conversation metadata invalidations once per changed page, including updates committed before a later failure. There is no new transport event or persistent worker cursor. A restart safely scans eligible rows again, and already Done rows are excluded.
24
+
25
+ ## Code and validation
26
+
27
+ - `src/conversations/auto-archive.ts`: scheduling, startup barrier, guardian snapshot and invalidation batching.
28
+ - `src/conversations/auto-archive-activity.ts`: synchronous runtime blockers.
29
+ - `src/persistence/conversation-auto-archive.ts`: shared SQL eligibility, paging and conditional writes.
30
+ - `src/persistence/conversation-crud.ts`: explicit reopen marker and existing activity resurfacing.
31
+
32
+ Focused tests cover eligibility and cursor races, reopen grace, paging, pending input, retries, lifecycle cancellation, coalescing and client invalidation. Migration tests verify existing data and idempotence.
@@ -53,8 +53,9 @@ Non-platform assistants keep their connected-computer behavior.
53
53
  Explicit `target: "connected-computer"` or `target_client_id` selects a connected
54
54
  computer on every client. The selected target never falls back to another
55
55
  computer when unavailable. The virtual desktop uses a fixed 1440x810 display.
56
- Observations return full-screen screenshots and mouse actions use the same
57
- screen coordinates.
56
+ Observations return full-screen screenshots and mouse actions use screen
57
+ points. Screenshot metadata uses the transport-optimized image dimensions and
58
+ provides per-axis conversion from image pixels to those screen points.
58
59
  Viewer resizing scales the canvas locally without changing the desktop layout.
59
60
 
60
61
  Observe first and pass the returned `observation_id` with each native action.
@@ -28,7 +28,8 @@
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
29
  "./platform-credential": "./src/platform-credential.ts",
30
30
  "./plan-credit": "./src/plan-credit.ts",
31
- "./integration-categories": "./src/integration-categories.ts"
31
+ "./integration-categories": "./src/integration-categories.ts",
32
+ "./mcp-oauth": "./src/mcp-oauth.ts"
32
33
  },
33
34
  "scripts": {
34
35
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildMcpOAuthCallbackUrl,
5
+ buildMcpOAuthClientMetadata,
6
+ buildMcpOAuthClientMetadataUrl,
7
+ } from "../mcp-oauth.js";
8
+
9
+ describe("MCP OAuth client metadata", () => {
10
+ test("builds the public client document", () => {
11
+ expect(
12
+ buildMcpOAuthClientMetadata({
13
+ clientId:
14
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
15
+ redirectUris: [
16
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
17
+ ],
18
+ }),
19
+ ).toEqual({
20
+ client_id:
21
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
22
+ redirect_uris: [
23
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
24
+ ],
25
+ token_endpoint_auth_method: "none",
26
+ grant_types: ["authorization_code", "refresh_token"],
27
+ response_types: ["code"],
28
+ client_name: "Vellum Assistant",
29
+ logo_uri: "https://www.vellum.ai/favicon.svg",
30
+ });
31
+ });
32
+
33
+ test("builds callback and metadata URLs from one normalized base", () => {
34
+ const baseUrl = "https://velay.example/assistant-123///";
35
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
36
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
37
+ );
38
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
39
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
40
+ );
41
+ });
42
+
43
+ test("canonicalizes equivalent public base URL spellings", () => {
44
+ const baseUrl = " https://VELAY.EXAMPLE:443/a/../assistant-123/// ";
45
+
46
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
47
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
48
+ );
49
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
50
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
51
+ );
52
+ });
53
+ });
@@ -30,6 +30,7 @@ export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
31
  export * from "./plan-credit.js";
32
32
  export * from "./integration-categories.js";
33
+ export * from "./mcp-oauth.js";
33
34
  export * from "./platform-credential.js";
34
35
  export * from "./remote-web-pairing.js";
35
36
  export * from "./twilio-ingress.js";
@@ -0,0 +1,60 @@
1
+ import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "./ingress.js";
2
+
3
+ export const MCP_OAUTH_CALLBACK_PATH = "/webhooks/oauth/callback";
4
+ export const MCP_OAUTH_CLIENT_METADATA_PATH = "/oauth/client-metadata.json";
5
+ export const MCP_OAUTH_CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
6
+
7
+ export interface McpOAuthClientMetadata {
8
+ client_id?: string;
9
+ redirect_uris: string[];
10
+ token_endpoint_auth_method: "none";
11
+ grant_types: ["authorization_code", "refresh_token"];
12
+ response_types: ["code"];
13
+ client_name: string;
14
+ logo_uri: string;
15
+ software_id?: string;
16
+ software_version?: string;
17
+ }
18
+
19
+ export interface BuildMcpOAuthClientMetadataOptions {
20
+ clientId?: string;
21
+ redirectUris: string[];
22
+ clientName?: string;
23
+ softwareId?: string;
24
+ softwareVersion?: string;
25
+ }
26
+
27
+ export function buildMcpOAuthClientMetadata(
28
+ options: BuildMcpOAuthClientMetadataOptions,
29
+ ): McpOAuthClientMetadata {
30
+ return {
31
+ ...(options.clientId && { client_id: options.clientId }),
32
+ redirect_uris: options.redirectUris,
33
+ token_endpoint_auth_method: "none",
34
+ grant_types: ["authorization_code", "refresh_token"],
35
+ response_types: ["code"],
36
+ client_name: options.clientName ?? "Vellum Assistant",
37
+ logo_uri: MCP_OAUTH_CLIENT_LOGO_URI,
38
+ ...(options.softwareId && { software_id: options.softwareId }),
39
+ ...(options.softwareVersion && {
40
+ software_version: options.softwareVersion,
41
+ }),
42
+ };
43
+ }
44
+
45
+ export function buildMcpOAuthCallbackUrl(publicBaseUrl: string): string {
46
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CALLBACK_PATH}`;
47
+ }
48
+
49
+ export function buildMcpOAuthClientMetadataUrl(publicBaseUrl: string): string {
50
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CLIENT_METADATA_PATH}`;
51
+ }
52
+
53
+ function normalizeBaseUrl(publicBaseUrl: string): string {
54
+ const normalized =
55
+ normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
56
+ if (!normalized) {
57
+ throw new Error("MCP OAuth public base URL must be an absolute HTTP URL");
58
+ }
59
+ return normalized;
60
+ }
@@ -3,18 +3,30 @@ import { describe, expect, test } from "bun:test";
3
3
  import {
4
4
  buildShellInvocation,
5
5
  buildShellSpawnFlags,
6
+ CHILD_OOM_SCORE_ADJ,
6
7
  pathListDelimiter,
7
8
  prependUniquePathEntries,
8
9
  } from "./shell.js";
9
10
 
10
11
  describe("buildShellInvocation", () => {
11
12
  test("uses Bash on POSIX hosts", () => {
12
- expect(buildShellInvocation("printf hello", "linux")).toEqual({
13
+ expect(buildShellInvocation("printf hello", "darwin")).toEqual({
13
14
  command: "bash",
14
15
  args: ["-c", "--", "printf hello"],
15
16
  });
16
17
  });
17
18
 
19
+ test("raises the shell's OOM-kill priority on Linux", () => {
20
+ expect(buildShellInvocation("printf hello", "linux")).toEqual({
21
+ command: "bash",
22
+ args: [
23
+ "-c",
24
+ "--",
25
+ `echo ${CHILD_OOM_SCORE_ADJ} 2>/dev/null >/proc/self/oom_score_adj; printf hello`,
26
+ ],
27
+ });
28
+ });
29
+
18
30
  test("uses encoded non-interactive PowerShell on Windows", () => {
19
31
  const invocation = buildShellInvocation(
20
32
  "Write-Output 'hello 世界'",
@@ -24,6 +24,23 @@ if (-not $__vellumCommandSucceeded) {
24
24
  }
25
25
  exit 0`;
26
26
 
27
+ /**
28
+ * `oom_score_adj` for assistant-owned tool processes. Any positive value
29
+ * outranks a 0-scored process; 1000 makes a tool the kernel's first choice
30
+ * whenever the container runs out of memory, so the assistant itself is never
31
+ * the victim of a runaway command.
32
+ */
33
+ export const CHILD_OOM_SCORE_ADJ = 1000;
34
+
35
+ /**
36
+ * Prefix for a Bash `-c` script that raises the shell's own OOM-kill priority
37
+ * before the command runs. Every process the command forks inherits it. Bash
38
+ * performs a builtin's redirection itself, so `/proc/self` is the shell.
39
+ * Redirections apply left to right, so stderr goes to /dev/null before the
40
+ * procfs open can fail and complain about it.
41
+ */
42
+ const LINUX_OOM_SCORE_ADJ_PREFIX = `echo ${CHILD_OOM_SCORE_ADJ} 2>/dev/null >/proc/self/oom_score_adj; `;
43
+
27
44
  export function buildShellInvocation(
28
45
  command: string,
29
46
  hostPlatform: NodeJS.Platform = process.platform,
@@ -45,7 +62,11 @@ export function buildShellInvocation(
45
62
  ],
46
63
  };
47
64
  }
48
- return { command: "bash", args: ["-c", "--", command] };
65
+ const script =
66
+ hostPlatform === "linux"
67
+ ? `${LINUX_OOM_SCORE_ADJ_PREFIX}${command}`
68
+ : command;
69
+ return { command: "bash", args: ["-c", "--", script] };
49
70
  }
50
71
 
51
72
  /**
@@ -28,7 +28,8 @@
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
29
  "./platform-credential": "./src/platform-credential.ts",
30
30
  "./plan-credit": "./src/plan-credit.ts",
31
- "./integration-categories": "./src/integration-categories.ts"
31
+ "./integration-categories": "./src/integration-categories.ts",
32
+ "./mcp-oauth": "./src/mcp-oauth.ts"
32
33
  },
33
34
  "scripts": {
34
35
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildMcpOAuthCallbackUrl,
5
+ buildMcpOAuthClientMetadata,
6
+ buildMcpOAuthClientMetadataUrl,
7
+ } from "../mcp-oauth.js";
8
+
9
+ describe("MCP OAuth client metadata", () => {
10
+ test("builds the public client document", () => {
11
+ expect(
12
+ buildMcpOAuthClientMetadata({
13
+ clientId:
14
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
15
+ redirectUris: [
16
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
17
+ ],
18
+ }),
19
+ ).toEqual({
20
+ client_id:
21
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
22
+ redirect_uris: [
23
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
24
+ ],
25
+ token_endpoint_auth_method: "none",
26
+ grant_types: ["authorization_code", "refresh_token"],
27
+ response_types: ["code"],
28
+ client_name: "Vellum Assistant",
29
+ logo_uri: "https://www.vellum.ai/favicon.svg",
30
+ });
31
+ });
32
+
33
+ test("builds callback and metadata URLs from one normalized base", () => {
34
+ const baseUrl = "https://velay.example/assistant-123///";
35
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
36
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
37
+ );
38
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
39
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
40
+ );
41
+ });
42
+
43
+ test("canonicalizes equivalent public base URL spellings", () => {
44
+ const baseUrl = " https://VELAY.EXAMPLE:443/a/../assistant-123/// ";
45
+
46
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
47
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
48
+ );
49
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
50
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
51
+ );
52
+ });
53
+ });
@@ -30,6 +30,7 @@ export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
31
  export * from "./plan-credit.js";
32
32
  export * from "./integration-categories.js";
33
+ export * from "./mcp-oauth.js";
33
34
  export * from "./platform-credential.js";
34
35
  export * from "./remote-web-pairing.js";
35
36
  export * from "./twilio-ingress.js";
@@ -0,0 +1,60 @@
1
+ import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "./ingress.js";
2
+
3
+ export const MCP_OAUTH_CALLBACK_PATH = "/webhooks/oauth/callback";
4
+ export const MCP_OAUTH_CLIENT_METADATA_PATH = "/oauth/client-metadata.json";
5
+ export const MCP_OAUTH_CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
6
+
7
+ export interface McpOAuthClientMetadata {
8
+ client_id?: string;
9
+ redirect_uris: string[];
10
+ token_endpoint_auth_method: "none";
11
+ grant_types: ["authorization_code", "refresh_token"];
12
+ response_types: ["code"];
13
+ client_name: string;
14
+ logo_uri: string;
15
+ software_id?: string;
16
+ software_version?: string;
17
+ }
18
+
19
+ export interface BuildMcpOAuthClientMetadataOptions {
20
+ clientId?: string;
21
+ redirectUris: string[];
22
+ clientName?: string;
23
+ softwareId?: string;
24
+ softwareVersion?: string;
25
+ }
26
+
27
+ export function buildMcpOAuthClientMetadata(
28
+ options: BuildMcpOAuthClientMetadataOptions,
29
+ ): McpOAuthClientMetadata {
30
+ return {
31
+ ...(options.clientId && { client_id: options.clientId }),
32
+ redirect_uris: options.redirectUris,
33
+ token_endpoint_auth_method: "none",
34
+ grant_types: ["authorization_code", "refresh_token"],
35
+ response_types: ["code"],
36
+ client_name: options.clientName ?? "Vellum Assistant",
37
+ logo_uri: MCP_OAUTH_CLIENT_LOGO_URI,
38
+ ...(options.softwareId && { software_id: options.softwareId }),
39
+ ...(options.softwareVersion && {
40
+ software_version: options.softwareVersion,
41
+ }),
42
+ };
43
+ }
44
+
45
+ export function buildMcpOAuthCallbackUrl(publicBaseUrl: string): string {
46
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CALLBACK_PATH}`;
47
+ }
48
+
49
+ export function buildMcpOAuthClientMetadataUrl(publicBaseUrl: string): string {
50
+ return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CLIENT_METADATA_PATH}`;
51
+ }
52
+
53
+ function normalizeBaseUrl(publicBaseUrl: string): string {
54
+ const normalized =
55
+ normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
56
+ if (!normalized) {
57
+ throw new Error("MCP OAuth public base URL must be an absolute HTTP URL");
58
+ }
59
+ return normalized;
60
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ FeatureFlagsIpcResponseSchema,
5
+ VelayStatusIpcResponseSchema,
6
+ } from "../gateway-ipc-contracts.js";
7
+
8
+ describe("gateway status IPC contracts", () => {
9
+ test("filters unsupported feature flag values independently", () => {
10
+ expect(
11
+ FeatureFlagsIpcResponseSchema.parse({
12
+ enabled: true,
13
+ model: "balanced",
14
+ count: 3,
15
+ nested: { enabled: true },
16
+ }),
17
+ ).toEqual({ enabled: true, model: "balanced" });
18
+ });
19
+
20
+ test("rejects non-record feature flag responses", () => {
21
+ expect(FeatureFlagsIpcResponseSchema.safeParse([]).success).toBe(false);
22
+ expect(FeatureFlagsIpcResponseSchema.safeParse(null).success).toBe(false);
23
+ });
24
+
25
+ test("normalizes a malformed Velay public URL to null", () => {
26
+ expect(
27
+ VelayStatusIpcResponseSchema.parse({
28
+ connected: false,
29
+ publicUrl: 42,
30
+ }),
31
+ ).toEqual({ connected: false, publicUrl: null });
32
+ });
33
+
34
+ test("requires a boolean Velay connection state", () => {
35
+ expect(
36
+ VelayStatusIpcResponseSchema.safeParse({ connected: "yes" }).success,
37
+ ).toBe(false);
38
+ });
39
+ });
@@ -17,6 +17,31 @@ export const GATEWAY_LOG_LEVEL_NAMES = [
17
17
  ] as const;
18
18
  export type GatewayLogLevelName = (typeof GATEWAY_LOG_LEVEL_NAMES)[number];
19
19
 
20
+ export const FeatureFlagsIpcResponseSchema = z
21
+ .record(z.string(), z.unknown())
22
+ .transform((flags): Record<string, boolean | string> => {
23
+ const parsed: Record<string, boolean | string> = {};
24
+ for (const [key, value] of Object.entries(flags)) {
25
+ if (typeof value === "boolean" || typeof value === "string") {
26
+ parsed[key] = value;
27
+ }
28
+ }
29
+ return parsed;
30
+ });
31
+
32
+ export type FeatureFlagsIpcResponse = z.infer<
33
+ typeof FeatureFlagsIpcResponseSchema
34
+ >;
35
+
36
+ export const VelayStatusIpcResponseSchema = z.object({
37
+ connected: z.boolean(),
38
+ publicUrl: z.string().nullable().catch(null),
39
+ });
40
+
41
+ export type VelayStatusIpcResponse = z.infer<
42
+ typeof VelayStatusIpcResponseSchema
43
+ >;
44
+
20
45
  const GatewayLogsTailIpcParamsShape = {
21
46
  n: z.number().int().min(1).max(1000).optional(),
22
47
  level: z.enum(GATEWAY_LOG_LEVEL_NAMES).optional(),
@@ -28,7 +28,8 @@
28
28
  "./guardian-requests": "./src/guardian-requests.ts",
29
29
  "./platform-credential": "./src/platform-credential.ts",
30
30
  "./plan-credit": "./src/plan-credit.ts",
31
- "./integration-categories": "./src/integration-categories.ts"
31
+ "./integration-categories": "./src/integration-categories.ts",
32
+ "./mcp-oauth": "./src/mcp-oauth.ts"
32
33
  },
33
34
  "scripts": {
34
35
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildMcpOAuthCallbackUrl,
5
+ buildMcpOAuthClientMetadata,
6
+ buildMcpOAuthClientMetadataUrl,
7
+ } from "../mcp-oauth.js";
8
+
9
+ describe("MCP OAuth client metadata", () => {
10
+ test("builds the public client document", () => {
11
+ expect(
12
+ buildMcpOAuthClientMetadata({
13
+ clientId:
14
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
15
+ redirectUris: [
16
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
17
+ ],
18
+ }),
19
+ ).toEqual({
20
+ client_id:
21
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
22
+ redirect_uris: [
23
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
24
+ ],
25
+ token_endpoint_auth_method: "none",
26
+ grant_types: ["authorization_code", "refresh_token"],
27
+ response_types: ["code"],
28
+ client_name: "Vellum Assistant",
29
+ logo_uri: "https://www.vellum.ai/favicon.svg",
30
+ });
31
+ });
32
+
33
+ test("builds callback and metadata URLs from one normalized base", () => {
34
+ const baseUrl = "https://velay.example/assistant-123///";
35
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
36
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
37
+ );
38
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
39
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
40
+ );
41
+ });
42
+
43
+ test("canonicalizes equivalent public base URL spellings", () => {
44
+ const baseUrl = " https://VELAY.EXAMPLE:443/a/../assistant-123/// ";
45
+
46
+ expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
47
+ "https://velay.example/assistant-123/webhooks/oauth/callback",
48
+ );
49
+ expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
50
+ "https://velay.example/assistant-123/oauth/client-metadata.json",
51
+ );
52
+ });
53
+ });
@@ -30,6 +30,7 @@ export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
31
  export * from "./plan-credit.js";
32
32
  export * from "./integration-categories.js";
33
+ export * from "./mcp-oauth.js";
33
34
  export * from "./platform-credential.js";
34
35
  export * from "./remote-web-pairing.js";
35
36
  export * from "./twilio-ingress.js";