@vellumai/assistant 0.11.4 → 0.11.5-dev.202608220019.a778cf0

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 (457) hide show
  1. package/AGENTS.md +6 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  15. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  19. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  20. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  21. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  22. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +30 -32
  23. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  24. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  25. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  26. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  28. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  29. package/openapi.yaml +487 -11
  30. package/package.json +1 -1
  31. package/scripts/sync-web-search-catalog.ts +6 -0
  32. package/src/__tests__/anthropic-provider.test.ts +10 -3
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  34. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  35. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  36. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  37. package/src/__tests__/channel-readiness-service.test.ts +126 -0
  38. package/src/__tests__/channel-reply-delivery.test.ts +30 -17
  39. package/src/__tests__/checker.test.ts +20 -314
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  41. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  42. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  44. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  45. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  46. package/src/__tests__/conversation-attachments.test.ts +0 -1
  47. package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
  48. package/src/__tests__/conversation-error.test.ts +17 -0
  49. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  50. package/src/__tests__/conversation-pairing.test.ts +186 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  52. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  53. package/src/__tests__/credential-routes.test.ts +22 -3
  54. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  55. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  56. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  57. package/src/__tests__/dm-backfill.test.ts +63 -0
  58. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  59. package/src/__tests__/edit-propagation.test.ts +107 -4
  60. package/src/__tests__/events-client-registration.test.ts +28 -0
  61. package/src/__tests__/file-write-tool.test.ts +4 -2
  62. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  63. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  64. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  65. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  66. package/src/__tests__/host-shell-tool.test.ts +23 -4
  67. package/src/__tests__/image-conversion.test.ts +143 -1
  68. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  69. package/src/__tests__/input-repairs.test.ts +207 -0
  70. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  71. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  72. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  73. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  74. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  75. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  76. package/src/__tests__/mtime-cache.test.ts +2 -0
  77. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  78. package/src/__tests__/openai-provider.test.ts +40 -0
  79. package/src/__tests__/openai-responses-provider.test.ts +18 -0
  80. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  81. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  82. package/src/__tests__/platform.test.ts +16 -1
  83. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  84. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  85. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  86. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  87. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  88. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  89. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  90. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  91. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  92. package/src/__tests__/reaction-persistence.test.ts +200 -7
  93. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  94. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  95. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  96. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  97. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  98. package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
  99. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  100. package/src/__tests__/settings-routes.test.ts +85 -3
  101. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  102. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  103. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  104. package/src/__tests__/tool-executor.test.ts +63 -38
  105. package/src/__tests__/tool-policy.test.ts +97 -1
  106. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  107. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  108. package/src/__tests__/validate-input.test.ts +243 -7
  109. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  110. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  111. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  112. package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
  113. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  114. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  115. package/src/acp/acp-claude-oauth.ts +38 -21
  116. package/src/acp/prepare-agent-env.ts +146 -55
  117. package/src/agent/loop-tool-dedup.test.ts +161 -0
  118. package/src/agent/loop.ts +68 -22
  119. package/src/api/constants/app-tools.ts +34 -0
  120. package/src/api/constants/profile-config-validation.ts +60 -0
  121. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  122. package/src/api/events/tool-result.ts +6 -1
  123. package/src/api/events/watch-retro-completed.ts +52 -0
  124. package/src/api/index.ts +30 -0
  125. package/src/api/responses/resource-pressure-status.ts +25 -0
  126. package/src/approvals/guardian-channel-delivery.ts +70 -6
  127. package/src/approvals/guardian-request-resolvers.ts +32 -66
  128. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  129. package/src/calls/voice-session-bridge.ts +60 -6
  130. package/src/channels/__tests__/message-audience.test.ts +49 -0
  131. package/src/channels/__tests__/types.test.ts +17 -3
  132. package/src/channels/message-audience.ts +40 -0
  133. package/src/channels/types.ts +16 -12
  134. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  135. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  136. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  137. package/src/cli/commands/__tests__/conversations-slack.test.ts +2 -1
  138. package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
  139. package/src/cli/commands/__tests__/inference-providers.test.ts +135 -2
  140. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  141. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  142. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  143. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  144. package/src/cli/commands/conversations.help.ts +34 -0
  145. package/src/cli/commands/conversations.ts +125 -0
  146. package/src/cli/commands/inference-profiles.ts +56 -3
  147. package/src/cli/commands/inference-providers.ts +37 -6
  148. package/src/cli/commands/inference.help.ts +9 -4
  149. package/src/cli/commands/keys.help.ts +13 -1
  150. package/src/cli/commands/keys.ts +26 -15
  151. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  152. package/src/cli/commands/memory/index.help.ts +41 -27
  153. package/src/cli/commands/memory/index.ts +2 -0
  154. package/src/cli/commands/memory/memory-v2.ts +58 -54
  155. package/src/cli/commands/memory/memory-validate.ts +18 -0
  156. package/src/cli/commands/monitoring.ts +1 -0
  157. package/src/cli/commands/oauth/request.test.ts +93 -0
  158. package/src/cli/commands/oauth/request.ts +23 -9
  159. package/src/cli/commands/plugins.help.ts +28 -31
  160. package/src/cli/commands/plugins.ts +10 -0
  161. package/src/cli/commands/skills.help.ts +13 -16
  162. package/src/cli/commands/trust.ts +3 -14
  163. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  164. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  165. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  166. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  167. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  168. package/src/cli/lib/bundled-marketplace.json +26 -0
  169. package/src/cli/lib/daemon-credential-client.ts +25 -3
  170. package/src/cli/lib/install-from-github.ts +35 -24
  171. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  172. package/src/cli/lib/open-browser.test.ts +67 -0
  173. package/src/cli/lib/open-browser.ts +24 -5
  174. package/src/cli/lib/plugin-surfaces.ts +27 -0
  175. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  176. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  177. package/src/config/__tests__/profile-materialization.test.ts +26 -0
  178. package/src/config/default-profile-catalog.ts +5 -6
  179. package/src/config/feature-flag-registry.json +36 -28
  180. package/src/config/profile-materialization.ts +29 -0
  181. package/src/config/sanitize-for-transfer.ts +16 -0
  182. package/src/config/schemas/llm.ts +7 -0
  183. package/src/config/schemas/services.ts +6 -0
  184. package/src/config/webhook-routing.ts +8 -0
  185. package/src/context/compactor.ts +31 -2
  186. package/src/context/outbound-sanitize.ts +6 -0
  187. package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
  188. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  189. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  190. package/src/daemon/conversation-error.ts +19 -2
  191. package/src/daemon/conversation-messaging.ts +22 -2
  192. package/src/daemon/conversation-process.ts +11 -0
  193. package/src/daemon/conversation-store.ts +10 -0
  194. package/src/daemon/conversation-surfaces.ts +21 -8
  195. package/src/daemon/conversation.ts +12 -10
  196. package/src/daemon/daemon-control.ts +1 -0
  197. package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
  198. package/src/daemon/handlers/config-ingress.ts +115 -5
  199. package/src/daemon/lifecycle.ts +30 -2
  200. package/src/daemon/message-types/conversations.ts +5 -7
  201. package/src/daemon/message-types/web-activity.ts +3 -2
  202. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  203. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  204. package/src/daemon/resource-pressure-guard.ts +387 -0
  205. package/src/daemon/shutdown-handlers.ts +2 -0
  206. package/src/daemon/startup-error.ts +16 -0
  207. package/src/daemon/trust-context.ts +14 -10
  208. package/src/daemon/unsendable-image-notice.ts +76 -0
  209. package/src/hooks/registry.ts +65 -51
  210. package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
  211. package/src/inbound/tunnel-probe.ts +261 -0
  212. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  213. package/src/ipc/gateway-client.test.ts +1 -1
  214. package/src/ipc/gateway-client.ts +14 -21
  215. package/src/ipc/socket-cleanup.ts +2 -11
  216. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  217. package/src/messaging/provider-message-metadata.ts +128 -0
  218. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +138 -73
  219. package/src/messaging/providers/channel-transport.ts +100 -16
  220. package/src/messaging/providers/discord/send.ts +16 -0
  221. package/src/messaging/providers/discord/transport.ts +23 -1
  222. package/src/messaging/providers/index.ts +84 -16
  223. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  224. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  225. package/src/messaging/providers/slack/send.test.ts +88 -34
  226. package/src/messaging/providers/slack/send.ts +99 -50
  227. package/src/messaging/providers/slack/transport.ts +35 -37
  228. package/src/messaging/providers/telegram-bot/send.test.ts +88 -18
  229. package/src/messaging/providers/telegram-bot/send.ts +36 -0
  230. package/src/messaging/providers/telegram-bot/transport.ts +22 -8
  231. package/src/messaging/read-provider-metadata.test.ts +157 -0
  232. package/src/messaging/read-provider-metadata.ts +51 -0
  233. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  234. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  235. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  236. package/src/notifications/adapters/macos.ts +55 -0
  237. package/src/notifications/adapters/slack.ts +10 -5
  238. package/src/notifications/assistant-reply-producer.ts +37 -4
  239. package/src/notifications/conversation-pairing.ts +111 -21
  240. package/src/notifications/edit-notification.ts +38 -8
  241. package/src/notifications/emit-signal.ts +12 -14
  242. package/src/notifications/home-feed-side-effect.ts +218 -15
  243. package/src/notifications/types.ts +5 -0
  244. package/src/permissions/AGENTS.md +16 -0
  245. package/src/permissions/checker.test.ts +75 -122
  246. package/src/permissions/checker.ts +44 -560
  247. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  248. package/src/permissions/confirmation-guardian-request.ts +5 -1
  249. package/src/persistence/conversation-crud.ts +69 -20
  250. package/src/persistence/conversation-queries.ts +14 -1
  251. package/src/persistence/conversation-types.ts +22 -0
  252. package/src/persistence/db-async-query.ts +2 -1
  253. package/src/persistence/db-maintenance.ts +15 -0
  254. package/src/persistence/delivery-crud.ts +122 -15
  255. package/src/persistence/embeddings/qdrant-manager.ts +1 -0
  256. package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
  257. package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
  258. package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
  259. package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
  260. package/src/persistence/schema/index.ts +1 -0
  261. package/src/persistence/schema/watch.ts +29 -0
  262. package/src/persistence/steps.ts +4 -0
  263. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  264. package/src/plugin-api/conversation-turn.ts +27 -0
  265. package/src/plugin-api/credential-scope.test.ts +15 -0
  266. package/src/plugin-api/credential-scope.ts +14 -0
  267. package/src/plugin-api/index.ts +19 -1
  268. package/src/plugin-api/resolve-credential.ts +15 -10
  269. package/src/plugin-api/store-credential.ts +148 -0
  270. package/src/plugin-api/system-card.ts +36 -0
  271. package/src/plugin-api/vision-support.test.ts +82 -0
  272. package/src/plugin-api/vision-support.ts +14 -2
  273. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  274. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  275. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  276. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  277. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  278. package/src/plugins/defaults/main.ts +15 -0
  279. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  280. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  281. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  282. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  283. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  284. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  286. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  287. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  288. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  289. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  290. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  291. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  292. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  293. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  294. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  295. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  296. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  297. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  298. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  299. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  300. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  301. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  302. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  303. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  304. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  305. package/src/plugins/mtime-cache.ts +1 -1
  306. package/src/plugins/pipeline.ts +14 -7
  307. package/src/plugins/plugin-execution-context.ts +35 -11
  308. package/src/plugins/plugin-tree-walk.ts +63 -4
  309. package/src/providers/__tests__/retry-network-error.test.ts +84 -0
  310. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  311. package/src/providers/anthropic/client.ts +332 -241
  312. package/src/providers/connection-resolution.ts +25 -2
  313. package/src/providers/content-blocks.ts +22 -0
  314. package/src/providers/fetch-provider-catalog.ts +19 -0
  315. package/src/providers/gemini/client.ts +5 -2
  316. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  317. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  318. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  319. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  320. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  321. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  322. package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
  323. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  324. package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
  325. package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
  326. package/src/providers/inference/adapter-factory.ts +33 -2
  327. package/src/providers/inference/auth.ts +13 -2
  328. package/src/providers/inference/credential-usage.ts +37 -0
  329. package/src/providers/inference/endpoint-probe.ts +115 -0
  330. package/src/providers/inference/missing-credential-guard.ts +110 -0
  331. package/src/providers/inference/profile-probe.ts +256 -0
  332. package/src/providers/inference/resolve-auth.ts +7 -7
  333. package/src/providers/media-resolve.ts +176 -15
  334. package/src/providers/model-catalog.ts +10 -0
  335. package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
  336. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  337. package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
  338. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  339. package/src/providers/openai/api-error-normalization.ts +16 -2
  340. package/src/providers/openai/chat-completions-provider.ts +326 -44
  341. package/src/providers/openai/responses-provider.ts +5 -2
  342. package/src/providers/provider-send-message.ts +11 -0
  343. package/src/providers/retry.ts +6 -0
  344. package/src/providers/search-provider-catalog.ts +20 -0
  345. package/src/routes/route-host-protocol.ts +7 -0
  346. package/src/routes/worker.ts +31 -10
  347. package/src/runtime/AGENTS.md +36 -0
  348. package/src/runtime/__tests__/host-observe.test.ts +302 -0
  349. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  350. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  351. package/src/runtime/assistant-event-hub.ts +38 -0
  352. package/src/runtime/channel-readiness-service.ts +39 -13
  353. package/src/runtime/channel-readiness-types.ts +49 -2
  354. package/src/runtime/channel-reply-delivery.ts +55 -32
  355. package/src/runtime/channel-retry-sweep.ts +6 -4
  356. package/src/runtime/effective-capabilities.test.ts +19 -0
  357. package/src/runtime/effective-capabilities.ts +25 -0
  358. package/src/runtime/guardian-reply-router.ts +6 -2
  359. package/src/runtime/host-observe.ts +246 -0
  360. package/src/runtime/http-errors.ts +1 -0
  361. package/src/runtime/http-server.ts +185 -6
  362. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  363. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  364. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
  365. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  366. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
  367. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +72 -1
  368. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
  369. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  370. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  371. package/src/runtime/routes/__tests__/plugins-routes.test.ts +103 -71
  372. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  373. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  374. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  375. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  376. package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
  377. package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
  378. package/src/runtime/routes/approval-routes.ts +37 -4
  379. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  380. package/src/runtime/routes/canned-message-complete.ts +68 -29
  381. package/src/runtime/routes/channel-readiness-routes.ts +14 -2
  382. package/src/runtime/routes/client-routes.ts +113 -0
  383. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  384. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  385. package/src/runtime/routes/conversation-query-routes.ts +10 -0
  386. package/src/runtime/routes/conversation-routes.ts +36 -22
  387. package/src/runtime/routes/credential-in-use.ts +85 -0
  388. package/src/runtime/routes/credential-routes.ts +55 -85
  389. package/src/runtime/routes/guardian-approval-interception.ts +44 -53
  390. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  391. package/src/runtime/routes/host-cu-routes.ts +18 -0
  392. package/src/runtime/routes/identity-routes.ts +5 -121
  393. package/src/runtime/routes/inbound-message-handler.ts +74 -30
  394. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  395. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +182 -297
  396. package/src/runtime/routes/inbound-stages/background-dispatch.ts +173 -386
  397. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  398. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  399. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  400. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  401. package/src/runtime/routes/index.ts +4 -0
  402. package/src/runtime/routes/inference-profiles-routes.ts +232 -31
  403. package/src/runtime/routes/inference-provider-connection-routes.ts +33 -11
  404. package/src/runtime/routes/ingress-status-routes.ts +180 -0
  405. package/src/runtime/routes/monitoring-routes.ts +9 -1
  406. package/src/runtime/routes/oauth-commands-routes.ts +1 -1
  407. package/src/runtime/routes/plugins-routes.ts +28 -32
  408. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  409. package/src/runtime/routes/secret-routes.ts +39 -8
  410. package/src/runtime/routes/settings-routes.ts +15 -12
  411. package/src/runtime/routes/surface-action-routes.ts +20 -19
  412. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  413. package/src/runtime/routes/user-route-resolution.ts +18 -1
  414. package/src/runtime/routes/watch-routes.ts +807 -0
  415. package/src/runtime/slack-reply-session.test.ts +23 -13
  416. package/src/runtime/slack-reply-session.ts +38 -55
  417. package/src/runtime/web-presence.ts +90 -0
  418. package/src/security/__tests__/untrusted-content.test.ts +42 -0
  419. package/src/security/untrusted-content.ts +28 -9
  420. package/src/skills/validate-input.ts +177 -40
  421. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  422. package/src/subagent/consult-context.ts +7 -23
  423. package/src/tools/credentials/broker.ts +24 -79
  424. package/src/tools/credentials/ref-parse.ts +35 -0
  425. package/src/tools/credentials/resolve.ts +4 -10
  426. package/src/tools/credentials/store.ts +168 -0
  427. package/src/tools/credentials/tool-policy.ts +67 -0
  428. package/src/tools/executor.ts +53 -15
  429. package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
  430. package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
  431. package/src/tools/network/__tests__/web-search.test.ts +138 -3
  432. package/src/tools/network/firecrawl-compat.ts +90 -0
  433. package/src/tools/network/url-safety.ts +9 -16
  434. package/src/tools/network/web-fetch.ts +142 -62
  435. package/src/tools/network/web-search.ts +162 -58
  436. package/src/tools/permission-checker.ts +43 -52
  437. package/src/tools/schema-transforms.ts +40 -5
  438. package/src/tools/shared/input-repairs.ts +160 -0
  439. package/src/tools/skills/skill-tool-factory.ts +18 -4
  440. package/src/tools/subagent/spawn.ts +0 -1
  441. package/src/tools/tool-approval-handler.ts +16 -10
  442. package/src/tools/tool-types.ts +26 -13
  443. package/src/tools/types.ts +6 -6
  444. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  445. package/src/util/cgroup-memory.ts +3 -0
  446. package/src/util/container-cpu-sampler.ts +250 -0
  447. package/src/util/image-conversion.ts +178 -14
  448. package/src/util/platform.ts +93 -10
  449. package/src/util/worker-process.ts +1 -0
  450. package/src/watch/__tests__/watch-retro.test.ts +665 -0
  451. package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
  452. package/src/watch/__tests__/watch-timeline.test.ts +670 -0
  453. package/src/watch/watch-retro.ts +480 -0
  454. package/src/watch/watch-session-manager.ts +575 -0
  455. package/src/watch/watch-timeline.ts +848 -0
  456. package/src/permissions/ipc-risk-types.ts +0 -143
  457. package/src/permissions/risk-types.ts +0 -76
@@ -91,6 +91,7 @@ import { ROUTES as INFERENCE_PROFILE_SESSION_ROUTES } from "./inference-profile-
91
91
  import { ROUTES as INFERENCE_PROFILES_ROUTES } from "./inference-profiles-routes.js";
92
92
  import { ROUTES as INFERENCE_PROVIDER_CONNECTION_ROUTES } from "./inference-provider-connection-routes.js";
93
93
  import { ROUTES as INFERENCE_SEND_ROUTES } from "./inference-send-routes.js";
94
+ import { ROUTES as INGRESS_STATUS_ROUTES } from "./ingress-status-routes.js";
94
95
  import { ROUTES as A2A_ROUTES } from "./integrations/a2a.js";
95
96
  import { ROUTES as SLACK_CHANNEL_CONFIG_ROUTES } from "./integrations/slack/channel.js";
96
97
  import { ROUTES as SLACK_CHANNELS_ROUTES } from "./integrations/slack/channels.js";
@@ -127,6 +128,7 @@ import { ROUTES as PUBLISH_ROUTES } from "./publish-routes.js";
127
128
  import { ROUTES as QUESTION_ROUTES } from "./question-routes.js";
128
129
  import { ROUTES as RECORDING_ROUTES } from "./recording-routes.js";
129
130
  import { ROUTES as RENAME_CONVERSATION_ROUTES } from "./rename-conversation-routes.js";
131
+ import { ROUTES as RESOURCE_PRESSURE_ROUTES } from "./resource-pressure-routes.js";
130
132
  import { ROUTES as RETROSPECTIVE_ROUTES } from "./retrospective-routes.js";
131
133
  import { ROUTES as ROUTE_HOST_WORKER_ROUTES } from "./route-host-worker-routes.js";
132
134
  import { ROUTES as SANITY_ROUTES } from "./sanity-routes.js";
@@ -210,6 +212,7 @@ export const ROUTES: RouteDefinition[] = [
210
212
  ...DEFAULT_PROVIDER_ROUTES,
211
213
  ...DIAGNOSTICS_ROUTES,
212
214
  ...DISK_PRESSURE_ROUTES,
215
+ ...RESOURCE_PRESSURE_ROUTES,
213
216
  ...DOMAIN_ROUTES,
214
217
  ...DOCUMENT_COMMENT_ROUTES,
215
218
  ...DOCUMENT_ROUTES,
@@ -238,6 +241,7 @@ export const ROUTES: RouteDefinition[] = [
238
241
  ...INFERENCE_PROFILES_ROUTES,
239
242
  ...INFERENCE_PROVIDER_CONNECTION_ROUTES,
240
243
  ...INFERENCE_SEND_ROUTES,
244
+ ...INGRESS_STATUS_ROUTES,
241
245
  ...INTERNAL_OAUTH_ROUTES,
242
246
  ...INTERNAL_TELEMETRY_ROUTES,
243
247
  ...MCP_AUTH_ROUTES,
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { z } from "zod";
20
20
 
21
+ import { validateInferenceProfileConfig } from "../../api/constants/profile-config-validation.js";
21
22
  import {
22
23
  getEffectiveProfilesForProvider,
23
24
  MANAGED_PROFILE_NAMES,
@@ -34,6 +35,7 @@ import {
34
35
  } from "../../config/schemas/llm.js";
35
36
  import { getDb } from "../../persistence/db-connection.js";
36
37
  import {
38
+ catalogProviderForProfile,
37
39
  resolveEntryProviderKind,
38
40
  writableProfileProviderIssue,
39
41
  } from "../../providers/connection-resolution.js";
@@ -45,7 +47,10 @@ import {
45
47
  isUnavailable,
46
48
  } from "../../providers/inference/connection-availability.js";
47
49
  import { getConnection } from "../../providers/inference/connections.js";
50
+ import { probeInferenceProfile } from "../../providers/inference/profile-probe.js";
48
51
  import {
52
+ catalogContextWindowTokens,
53
+ catalogMaxOutputTokens,
49
54
  getModelDisplayName,
50
55
  isModelInCatalog,
51
56
  } from "../../providers/model-catalog.js";
@@ -82,6 +87,18 @@ const availabilitySchema = z
82
87
  })
83
88
  .meta({ id: "ProfileConnectionAvailability" });
84
89
 
90
+ /**
91
+ * Static config problem with the stored entry itself (unknown model,
92
+ * impossible token budget), distinct from `availability` which judges the
93
+ * connection and credential behind it. Absent when the config checks out.
94
+ */
95
+ const profileConfigIssueSchema = z
96
+ .object({
97
+ code: z.enum(["model_unknown", "over_output_cap", "no_input_room"]),
98
+ message: z.string(),
99
+ })
100
+ .meta({ id: "InferenceProfileConfigIssue" });
101
+
85
102
  const profileSummarySchema = z
86
103
  .object({
87
104
  name: z.string(),
@@ -93,6 +110,7 @@ const profileSummarySchema = z
93
110
  provider_connection: z.string().optional(),
94
111
  /** Null when the profile has no provider to judge (e.g. mix profiles). */
95
112
  availability: availabilitySchema.nullable(),
113
+ config_issue: profileConfigIssueSchema.optional(),
96
114
  })
97
115
  .meta({ id: "InferenceProfileSummary" });
98
116
 
@@ -101,6 +119,7 @@ const profileDetailSchema = z
101
119
  name: z.string(),
102
120
  entry: z.record(z.string(), z.unknown()),
103
121
  availability: availabilitySchema.nullable(),
122
+ config_issue: profileConfigIssueSchema.optional(),
104
123
  })
105
124
  .meta({ id: "InferenceProfileDetail" });
106
125
 
@@ -119,6 +138,17 @@ const profileWriteResultSchema = z
119
138
  })
120
139
  .meta({ id: "InferenceProfileWriteResult" });
121
140
 
141
+ const profileCheckSchema = z
142
+ .object({
143
+ ok: z.boolean(),
144
+ blame: z.enum(["profile", "provider", "transient", "unknown"]).optional(),
145
+ reason: z.string().optional(),
146
+ detail: z.string().optional(),
147
+ connection: z.string().optional(),
148
+ message: z.string().optional(),
149
+ })
150
+ .meta({ id: "InferenceProfileCheck" });
151
+
122
152
  const createRequestSchema = z.object({
123
153
  name: z.string().min(1),
124
154
  provider: z.string().min(1),
@@ -160,63 +190,147 @@ function assertValidProvider(provider: string): void {
160
190
  }
161
191
 
162
192
  /**
163
- * Validate a (provider, model) pair against the catalog. Returns warnings
164
- * (never throws) when `allowUnlisted`; throws otherwise for an uncataloged
165
- * model. An uncataloged model always warns, whether or not it is allowed.
193
+ * Why a (provider, model, connection) triple cannot be vouched for, or null.
194
+ * The same reach checks dispatch applies: routing identities against their
195
+ * routing table, entry names against their row's dispatchable kind, vendor
196
+ * ids against the catalog, with the named connection's advertised model list
197
+ * authoritative for models the code-owned catalog doesn't know (a custom
198
+ * endpoint declares its own models at connection-create time).
166
199
  */
167
- function validateModel(
200
+ function modelReachIssue(
168
201
  provider: string,
169
202
  model: string,
170
- allowUnlisted: boolean,
171
203
  connectionName?: string,
172
- ): string[] {
173
- // Routing identities key no catalog entries; they validate against their
174
- // route's actual reach — the same checks dispatch applies per-request.
175
- // allowUnlisted deliberately does not apply: the routing table ships in
176
- // this build, so an unroutable pair fails every request, and the schema
177
- // strips it on the next config read.
204
+ ): { identity: boolean; catalogProvider: string; message: string } | null {
178
205
  if (ROUTING_IDENTITY_PROVIDERS.has(provider)) {
179
206
  const issue = routingIdentityModelIssue(provider, model);
180
- if (issue) {
181
- throw new BadRequestError(
182
- `${issue} Pick a model this route serves, or a concrete provider.`,
183
- );
184
- }
185
- return [];
207
+ return issue
208
+ ? { identity: true, catalogProvider: provider, message: issue }
209
+ : null;
186
210
  }
187
- // An entry-name provider validates against its row's dispatchable kind,
188
- // the same translation dispatch uses; the entry itself also serves as the
189
- // model-list connection for custom endpoints below.
190
211
  const entryKind = resolveEntryProviderKind(provider, model);
191
212
  const catalogProvider = entryKind ?? provider;
192
213
  if (isModelInCatalog(catalogProvider, model)) {
193
- return [];
214
+ return null;
194
215
  }
195
- // The named connection's advertised model list is authoritative for models
196
- // the code-owned catalog doesn't know — a custom (openai-compatible)
197
- // endpoint declares its own models at connection-create time.
198
216
  const modelListConnection =
199
217
  connectionName ?? (entryKind !== null ? provider : undefined);
200
218
  if (modelListConnection) {
201
219
  const connection = getConnection(getDb(), modelListConnection);
202
220
  if (connection?.models?.some((m) => m.id === model)) {
203
- return [];
221
+ return null;
204
222
  }
205
223
  }
224
+ return {
225
+ identity: false,
226
+ catalogProvider,
227
+ message: `Model "${model}" is not in the catalog for provider "${catalogProvider}".`,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Validate a (provider, model) pair against the catalog. Returns warnings
233
+ * (never throws) when `allowUnlisted`; throws otherwise for an uncataloged
234
+ * model. An uncataloged model always warns, whether or not it is allowed.
235
+ */
236
+ function validateModel(
237
+ provider: string,
238
+ model: string,
239
+ allowUnlisted: boolean,
240
+ connectionName?: string,
241
+ ): string[] {
242
+ const issue = modelReachIssue(provider, model, connectionName);
243
+ if (!issue) {
244
+ return [];
245
+ }
246
+ // allowUnlisted deliberately does not apply to routing identities: the
247
+ // routing table ships in this build, so an unroutable pair fails every
248
+ // request, and the schema strips it on the next config read.
249
+ if (issue.identity) {
250
+ throw new BadRequestError(
251
+ `${issue.message} Pick a model this route serves, or a concrete provider.`,
252
+ );
253
+ }
206
254
  if (!allowUnlisted) {
207
255
  const remedy =
208
- catalogProvider === "openai-compatible"
256
+ issue.catalogProvider === "openai-compatible"
209
257
  ? `Pass allowUnlisted to create it anyway, or declare the model on the connection ` +
210
258
  `("assistant inference providers update <name> --model ${model}").`
211
259
  : `Pass allowUnlisted to create it anyway, or run ` +
212
260
  `"assistant inference models list --provider ${provider}" to see valid ids.`;
213
- throw new BadRequestError(
214
- `Model "${model}" is not in the catalog for provider "${catalogProvider}". ${remedy}`,
261
+ throw new BadRequestError(`${issue.message} ${remedy}`);
262
+ }
263
+ return [`${issue.message} Created anyway (allowUnlisted).`];
264
+ }
265
+
266
+ /**
267
+ * Static config verdict for a stored profile: the model-reach and
268
+ * token-budget judgments the write routes enforce, recomputed over the
269
+ * entry so rows that predate validation (or were written through the
270
+ * generic config escape hatch) surface their problem in listings. Cheap
271
+ * and offline; the live probe covers what only a request can prove. Mix
272
+ * arms are judged on their own rows, and managed bodies are code-owned.
273
+ */
274
+ function profileConfigIssue(record: Record<string, unknown>): {
275
+ code: "model_unknown" | "over_output_cap" | "no_input_room";
276
+ message: string;
277
+ } | null {
278
+ if (record.mix != null || record.source === "managed") {
279
+ return null;
280
+ }
281
+ const provider =
282
+ typeof record.provider === "string" ? record.provider : undefined;
283
+ const model = typeof record.model === "string" ? record.model : undefined;
284
+ if (!provider || !model) {
285
+ // A missing provider/model is availability's `incomplete` verdict.
286
+ return null;
287
+ }
288
+ // A stored allowUnlisted marker records that catalog absence was accepted
289
+ // deliberately at write time; the live probe is the check for those.
290
+ if (record.allowUnlisted !== true) {
291
+ const reach = modelReachIssue(
292
+ provider,
293
+ model,
294
+ typeof record.provider_connection === "string"
295
+ ? record.provider_connection
296
+ : undefined,
215
297
  );
298
+ if (reach) {
299
+ return { code: "model_unknown", message: reach.message };
300
+ }
301
+ }
302
+ if (typeof record.maxTokens === "number") {
303
+ const budget = maxTokensBudgetIssue(provider, model, record.maxTokens);
304
+ if (budget) {
305
+ return { code: budget.code, message: budget.message };
306
+ }
216
307
  }
217
- return [
218
- `Model "${model}" is not in the catalog for provider "${catalogProvider}"; created anyway (allowUnlisted).`,
219
- ];
308
+ return null;
309
+ }
310
+
311
+ /**
312
+ * The shared token-budget judgment over a (provider, model, maxTokens)
313
+ * triple with the catalog-provider translation applied. Null when the budget
314
+ * passes or the catalog knows nothing to judge against. One implementation
315
+ * for the write routes and the listing verdict so the two cannot drift.
316
+ */
317
+ function maxTokensBudgetIssue(
318
+ provider: string,
319
+ model: string,
320
+ maxTokens: number,
321
+ ): ReturnType<typeof validateInferenceProfileConfig> {
322
+ const catalogProvider = catalogProviderForProfile(provider, model);
323
+ if (catalogProvider === null) {
324
+ return null;
325
+ }
326
+ return validateInferenceProfileConfig({
327
+ maxTokens,
328
+ modelMaxOutputTokens: catalogMaxOutputTokens(catalogProvider, model),
329
+ modelContextWindowTokens: catalogContextWindowTokens(
330
+ catalogProvider,
331
+ model,
332
+ ),
333
+ });
220
334
  }
221
335
 
222
336
  function assertConnectionExists(name: string): void {
@@ -373,6 +487,27 @@ function validateProfileEntry(entry: Record<string, unknown>): void {
373
487
  }
374
488
  }
375
489
 
490
+ /**
491
+ * Reject an explicit `maxTokens` the catalog can prove impossible for the
492
+ * model (over its output cap, or reserving the whole context window so no
493
+ * input fits). Judged against the same catalog-provider translation
494
+ * `validateModel` uses; models the catalog does not know are left to the
495
+ * live probe.
496
+ */
497
+ function assertSaneMaxTokens(
498
+ provider: string,
499
+ model: string,
500
+ maxTokens: number | undefined,
501
+ ): void {
502
+ if (maxTokens === undefined) {
503
+ return;
504
+ }
505
+ const issue = maxTokensBudgetIssue(provider, model, maxTokens);
506
+ if (issue) {
507
+ throw new BadRequestError(issue.message);
508
+ }
509
+ }
510
+
376
511
  // ---------------------------------------------------------------------------
377
512
  // Handlers
378
513
  // ---------------------------------------------------------------------------
@@ -386,6 +521,7 @@ async function handleListProfiles() {
386
521
  const profiles = await Promise.all(
387
522
  Object.entries(effective).map(async ([name, entry]) => {
388
523
  const record = entry as Record<string, unknown>;
524
+ const configIssue = profileConfigIssue(record);
389
525
  return {
390
526
  name,
391
527
  label: typeof record.label === "string" ? record.label : null,
@@ -397,6 +533,7 @@ async function handleListProfiles() {
397
533
  ? { provider_connection: record.provider_connection }
398
534
  : {}),
399
535
  availability: await computeProfileAvailability(record),
536
+ ...(configIssue ? { config_issue: configIssue } : {}),
400
537
  };
401
538
  }),
402
539
  );
@@ -418,10 +555,12 @@ async function handleGetProfile({ pathParams = {} }: RouteHandlerArgs) {
418
555
  throw new NotFoundError(`Profile "${name}" not found.`);
419
556
  }
420
557
  const record = entry as Record<string, unknown>;
558
+ const configIssue = profileConfigIssue(record);
421
559
  return {
422
560
  name,
423
561
  entry: record,
424
562
  availability: await computeProfileAvailability(record),
563
+ ...(configIssue ? { config_issue: configIssue } : {}),
425
564
  };
426
565
  }
427
566
 
@@ -456,12 +595,19 @@ async function handleCreateProfile({ body = {} }: RouteHandlerArgs) {
456
595
  input.allowUnlisted ?? false,
457
596
  input.connection,
458
597
  );
598
+ assertSaneMaxTokens(input.provider, input.model, input.maxTokens);
459
599
 
460
600
  const entry: Record<string, unknown> = {
461
601
  ...fragmentFromBody(body as Record<string, unknown>),
462
602
  provider: input.provider,
463
603
  model: input.model,
464
604
  source: "user",
605
+ // `warnings` here can only be validateModel's unlisted verdict (the
606
+ // availability warning pushes later), so its presence is exactly the
607
+ // deliberate allowUnlisted acceptance the listing verdict must honor.
608
+ ...(input.allowUnlisted && warnings.length > 0
609
+ ? { allowUnlisted: true }
610
+ : {}),
465
611
  };
466
612
  // Pickers render the label, so a label-less profile shows its raw config
467
613
  // key (e.g. "gemini-latest"). Default it to the model's human-readable
@@ -585,6 +731,24 @@ async function handleUpdateProfile({
585
731
  nextConnection,
586
732
  );
587
733
  }
734
+ // Gated on the fields that feed the judgment, mirroring the availability
735
+ // guard: metadata-only edits must not start rejecting a stored budget.
736
+ if (
737
+ (input.maxTokens !== undefined ||
738
+ input.model !== undefined ||
739
+ input.provider !== undefined) &&
740
+ typeof nextProvider === "string" &&
741
+ typeof nextModel === "string"
742
+ ) {
743
+ assertSaneMaxTokens(
744
+ nextProvider,
745
+ nextModel,
746
+ input.maxTokens ??
747
+ (typeof existing.maxTokens === "number"
748
+ ? existing.maxTokens
749
+ : undefined),
750
+ );
751
+ }
588
752
 
589
753
  const merged: Record<string, unknown> = {
590
754
  ...existing,
@@ -594,6 +758,17 @@ async function handleUpdateProfile({
594
758
  source:
595
759
  existing.source === "managed" ? "user" : (existing.source ?? "user"),
596
760
  };
761
+ // Keep the allowUnlisted marker faithful to the pair being stored: stamp
762
+ // it on a deliberate unlisted acceptance (warnings can only be the
763
+ // unlisted verdict here), and drop a stale one when the pair is now
764
+ // vouched for. Untouched pairs keep their stored marker.
765
+ if (input.provider !== undefined || input.model !== undefined) {
766
+ if (input.allowUnlisted && warnings.length > 0) {
767
+ merged.allowUnlisted = true;
768
+ } else if (warnings.length === 0) {
769
+ delete merged.allowUnlisted;
770
+ }
771
+ }
597
772
  validateProfileEntry(merged);
598
773
 
599
774
  // The availability guard runs only when the write touches the fields that
@@ -632,6 +807,14 @@ async function handleUpdateProfile({
632
807
  };
633
808
  }
634
809
 
810
+ async function handleValidateProfile({ pathParams = {} }: RouteHandlerArgs) {
811
+ const name = (pathParams.name ?? "").trim();
812
+ if (!name) {
813
+ throw new BadRequestError("Profile name must be a non-empty string");
814
+ }
815
+ return { check: await probeInferenceProfile(name) };
816
+ }
817
+
635
818
  async function handleDeleteProfile({ pathParams = {} }: RouteHandlerArgs) {
636
819
  const name = (pathParams.name ?? "").trim();
637
820
  if (!name) {
@@ -858,4 +1041,22 @@ export const ROUTES: RouteDefinition[] = [
858
1041
  },
859
1042
  handler: handleSetActiveProfile,
860
1043
  },
1044
+ {
1045
+ operationId: "inference_profiles_validate",
1046
+ endpoint: "inference/profiles/:name/validate",
1047
+ method: "POST",
1048
+ policy: {
1049
+ requiredScopes: ["settings.write"],
1050
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
1051
+ },
1052
+ summary: "Probe a saved profile with one minimal request",
1053
+ description:
1054
+ "Dispatch one minimal test request through the named profile's resolved model and connection, and classify any failure by which object the user should fix (the profile vs its provider connection). Advisory: the probe never mutates the profile. Returns a null check when there is no verdict to give (missing, disabled, managed, or routing-identity profiles, or a probe timeout). The probe spends one tiny request on the profile's own key.",
1055
+ tags: ["inference"],
1056
+ pathParams: [{ name: "name", description: "Profile name" }],
1057
+ responseBody: z.object({
1058
+ check: profileCheckSchema.nullable(),
1059
+ }),
1060
+ handler: handleValidateProfile,
1061
+ },
861
1062
  ];
@@ -27,7 +27,7 @@ import {
27
27
  ConnectionProviderSchema,
28
28
  deriveAuthForProvider,
29
29
  ProviderConnectionSchema,
30
- PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
30
+ PROVIDERS_ALLOWING_CUSTOM_BASE_URL,
31
31
  VALID_CONNECTION_PROVIDERS,
32
32
  } from "../../providers/inference/auth.js";
33
33
  import {
@@ -39,6 +39,10 @@ import {
39
39
  MANAGED_CONNECTION_NAMES,
40
40
  updateConnection,
41
41
  } from "../../providers/inference/connections.js";
42
+ import {
43
+ EndpointCheckSchema,
44
+ testInferenceConnection,
45
+ } from "../../providers/inference/endpoint-probe.js";
42
46
  import { PROVIDER_CATALOG } from "../../providers/model-catalog.js";
43
47
  import {
44
48
  isVellumManagedConnection,
@@ -64,6 +68,16 @@ const log = getLogger("routes/inference-provider-connections");
64
68
 
65
69
  const providerConnectionResponseSchema = ProviderConnectionSchema;
66
70
 
71
+ /**
72
+ * Create/update responses carry the save-time endpoint probe result for
73
+ * connections with a custom base URL. Advisory only: a failed probe never
74
+ * fails the save (some endpoints legitimately reject unauthenticated or
75
+ * minimal requests); clients render `hint` as a warning.
76
+ */
77
+ const savedConnectionResponseSchema = ProviderConnectionSchema.extend({
78
+ endpoint_check: EndpointCheckSchema.optional(),
79
+ }).meta({ id: "SavedProviderConnection" });
80
+
67
81
  // ---------------------------------------------------------------------------
68
82
  // Custom provider field parsing (openai-compatible base_url + models)
69
83
  // ---------------------------------------------------------------------------
@@ -72,14 +86,16 @@ const providerConnectionResponseSchema = ProviderConnectionSchema;
72
86
  * Parse and validate `base_url` and `models` from the request body.
73
87
  *
74
88
  * `base_url` is only accepted for providers in
75
- * `PROVIDERS_REQUIRING_BASE_URL_AND_MODELS` (currently `openai-compatible`).
89
+ * `PROVIDERS_ALLOWING_CUSTOM_BASE_URL` (openai-compatible and ollama).
76
90
  * For all other providers, supplying `base_url` returns a 400. This prevents
77
91
  * API-key exfiltration: an attacker cannot create an `anthropic` connection
78
92
  * with a `base_url` pointing to their own server, which would redirect all
79
93
  * LLM calls (and the API key) to the attacker.
80
94
  *
81
- * Even for `openai-compatible`, the `base_url` must not point to private
82
- * networks or cloud metadata endpoints (SSRF protection).
95
+ * Even for allowed providers, a platform-hosted daemon rejects `base_url`
96
+ * values that point to private networks or cloud metadata endpoints
97
+ * (SSRF protection). Self-hosted daemons allow those addresses because
98
+ * localhost and LAN hosts are the expected target.
83
99
  */
84
100
  async function parseCustomProviderFields(
85
101
  body: Record<string, unknown>,
@@ -96,14 +112,14 @@ async function parseCustomProviderFields(
96
112
  if ("base_url" in body) {
97
113
  const raw = body.base_url;
98
114
 
99
- // Gate: base_url is only valid for openai-compatible providers.
115
+ // Gate: base_url is only valid for providers that persist a custom endpoint.
100
116
  if (
101
117
  raw !== null &&
102
118
  raw !== undefined &&
103
- !PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(provider)
119
+ !PROVIDERS_ALLOWING_CUSTOM_BASE_URL.has(provider)
104
120
  ) {
105
121
  throw new BadRequestError(
106
- `base_url is only valid for openai-compatible providers. Remove base_url or use the openai-compatible provider type.`,
122
+ `base_url is only valid for openai-compatible and ollama providers. Remove base_url or use a provider that accepts a custom endpoint.`,
107
123
  );
108
124
  }
109
125
 
@@ -426,7 +442,10 @@ async function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
426
442
  throw new BadRequestError("Invalid auth configuration.");
427
443
  }
428
444
 
429
- return result.connection;
445
+ const endpointCheck = await testInferenceConnection(result.connection);
446
+ return endpointCheck
447
+ ? { ...result.connection, endpoint_check: endpointCheck }
448
+ : result.connection;
430
449
  }
431
450
 
432
451
  async function handleUpdateConnection({
@@ -545,7 +564,10 @@ async function handleUpdateConnection({
545
564
  throw new BadRequestError("Invalid auth configuration.");
546
565
  }
547
566
 
548
- return result.connection;
567
+ const endpointCheck = await testInferenceConnection(result.connection);
568
+ return endpointCheck
569
+ ? { ...result.connection, endpoint_check: endpointCheck }
570
+ : result.connection;
549
571
  }
550
572
 
551
573
  async function handleDeleteConnection({ pathParams = {} }: RouteHandlerArgs) {
@@ -749,7 +771,7 @@ export const ROUTES: RouteDefinition[] = [
749
771
  base_url: z.string().url().nullable().optional(),
750
772
  models: z.array(ConnectionModelSchema).nullable().optional(),
751
773
  }),
752
- responseBody: providerConnectionResponseSchema,
774
+ responseBody: savedConnectionResponseSchema,
753
775
  responseStatus: "201",
754
776
  additionalResponses: {
755
777
  "400": { description: "Invalid provider or auth schema" },
@@ -777,7 +799,7 @@ export const ROUTES: RouteDefinition[] = [
777
799
  base_url: z.string().url().nullable().optional(),
778
800
  models: z.array(ConnectionModelSchema).nullable().optional(),
779
801
  }),
780
- responseBody: providerConnectionResponseSchema,
802
+ responseBody: savedConnectionResponseSchema,
781
803
  additionalResponses: {
782
804
  "400": {
783
805
  description: