@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
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Reachability probe for the public URL a tunnel exposes.
3
+ *
4
+ * A tunnel's public URL is recorded in the workspace config, but a recorded
5
+ * URL says nothing about whether anything still answers on it: the tunnel
6
+ * process can die, the nginx edge can stay up while the gateway behind it is
7
+ * down, and a URL can end up fronting a different assistant entirely. The
8
+ * probe asks the edge two questions at once, and the two answers carry very
9
+ * different weight:
10
+ *
11
+ * - `GET /healthz` is the liveness verdict. Every ingress shape answers it:
12
+ * the nginx edge proxies it to the gateway, and a tunnel pointed straight
13
+ * at the gateway port serves it from the gateway itself. Anything but a
14
+ * 2xx means the URL is not usable at all, which is `unreachable`.
15
+ * - `GET /assistant/__config` decides whether that live edge is one a device
16
+ * can pair against. Only the nginx remote-web SPA edge serves it
17
+ * (`buildRemoteWebIngressLocations` in `cli/src/lib/nginx-ingress.ts`),
18
+ * and that is the same edge serving `/assistant/pair`, so an edge that
19
+ * answers the config request with anything but that edge's own config is
20
+ * an edge whose pair URL would 404. That is `unpairable`: alive, and no
21
+ * use to a card whose whole job is pairing. A tunnel pointed straight at
22
+ * the gateway, a bring-your-own HTTPS front that only proxies the gateway,
23
+ * and a catch-all that answers every path with some JSON all land there.
24
+ * A config request that gets no answer at all, because it lost the shared
25
+ * deadline or the connection dropped under it, proves nothing either way,
26
+ * and the live `/healthz` stands.
27
+ *
28
+ * The served config also carries the assistant's id, which is what separates
29
+ * `healthy` from `foreign`.
30
+ *
31
+ * Neither path is denylisted, so both are publicly reachable.
32
+ *
33
+ * The module takes a URL and an expected id and returns a verdict: it reads no
34
+ * config and knows nothing about workspace paths, which is what lets callers
35
+ * probe an arbitrary URL and lets tests run without network access.
36
+ */
37
+
38
+ import {
39
+ normalizeHttpPublicBaseUrlWithoutTrailingSlash,
40
+ trimmedNonEmptyString,
41
+ } from "@vellumai/service-contracts/ingress";
42
+
43
+ import { isPlainObject } from "../util/object.js";
44
+ import { truncate } from "../util/truncate.js";
45
+
46
+ /**
47
+ * Verdict for one probe.
48
+ *
49
+ * `unpairable` means the edge answered the config request with something that
50
+ * is not the pairing edge's config, so it is alive without fronting the
51
+ * pairing app.
52
+ * `foreign` means the edge positively identified itself as a different
53
+ * assistant. Any case where either id is unknown is `healthy`: a config
54
+ * served by a CLI that predates the id, or a URL recorded before the id was
55
+ * saved, is version skew, not someone else's assistant.
56
+ */
57
+ export type TunnelProbeResult =
58
+ | { kind: "healthy"; assistantId?: string; assistantName?: string }
59
+ | { kind: "unreachable"; detail: string }
60
+ | { kind: "unpairable"; detail?: string }
61
+ | { kind: "foreign"; assistantId?: string; assistantName?: string };
62
+
63
+ const DEFAULT_TIMEOUT_MS = 4_000;
64
+
65
+ /** Placeholder swapped in for the probed URL so `detail` stays URL-free. */
66
+ const REDACTED_URL = "<url>";
67
+
68
+ /** `detail` is rendered inline in a settings card, so it has to stay short. */
69
+ const MAX_DETAIL_LENGTH = 120;
70
+
71
+ /** Bound on the `cause` walk, so a self-referential chain cannot spin. */
72
+ const MAX_CAUSE_DEPTH = 5;
73
+
74
+ /** Wrappers fetch layers raise when the real reason sits one `cause` down. */
75
+ const GENERIC_FETCH_MESSAGES = new Set(["fetch failed", "failed to fetch"]);
76
+
77
+ /** Reported when the config path answers with a body that is not a config. */
78
+ const UNREADABLE_CONFIG_DETAIL = "no assistant config served";
79
+
80
+ /** Reported when a served body parses but is not the pairing edge's config. */
81
+ const UNMARKED_CONFIG_DETAIL = "not a pairing edge config";
82
+
83
+ /**
84
+ * Stamped into the served config by the nginx pairing edge and nothing else
85
+ * (`remoteWebIngressConfig` in `cli/src/lib/nginx-ingress.ts`), which makes it
86
+ * the marker that separates that edge from any other JSON a live URL serves.
87
+ */
88
+ const PAIRING_EDGE_MODE = "remote-gateway";
89
+
90
+ export async function probeTunnel(args: {
91
+ publicBaseUrl: string;
92
+ /** Id recorded when this URL was saved; omit to skip the identity check. */
93
+ expectedAssistantId?: string;
94
+ timeoutMs?: number;
95
+ fetchImpl?: typeof fetch;
96
+ }): Promise<TunnelProbeResult> {
97
+ if (args.publicBaseUrl.trim().length === 0) {
98
+ return { kind: "unreachable", detail: "no public base URL" };
99
+ }
100
+ // The same validation the config writers apply, so a value they would have
101
+ // rejected is named as malformed rather than handed to `fetch` and reported
102
+ // through whatever string that layer happens to produce.
103
+ const base = normalizeHttpPublicBaseUrlWithoutTrailingSlash(
104
+ args.publicBaseUrl,
105
+ );
106
+ if (base === undefined) {
107
+ return { kind: "unreachable", detail: "not an http(s) URL" };
108
+ }
109
+
110
+ const fetchImpl = args.fetchImpl ?? globalThis.fetch.bind(globalThis);
111
+ // One deadline covers both requests: only an answered config request can
112
+ // demote a live edge, so a slow edge that answers `/healthz` and loses the
113
+ // config request to the same deadline keeps its liveness verdict.
114
+ const signal = AbortSignal.timeout(args.timeoutMs ?? DEFAULT_TIMEOUT_MS);
115
+ const [health, config] = await Promise.allSettled([
116
+ fetchImpl(`${base}/healthz`, { signal }),
117
+ fetchImpl(`${base}/assistant/__config`, { signal }),
118
+ ]);
119
+ const configResponse =
120
+ config.status === "fulfilled" ? config.value : undefined;
121
+
122
+ if (health.status === "rejected") {
123
+ await discardBody(configResponse);
124
+ return unreachable(describeError(health.reason), base);
125
+ }
126
+ // Liveness is the status line; the health body is never read.
127
+ await discardBody(health.value);
128
+ if (!health.value.ok) {
129
+ await discardBody(configResponse);
130
+ return unreachable(`HTTP ${health.value.status}`, base);
131
+ }
132
+
133
+ const served = await readServedConfig(configResponse);
134
+ if (served.kind === "notServed") {
135
+ return { kind: "unpairable", detail: served.detail };
136
+ }
137
+
138
+ const identity = served.kind === "served" ? served.config : {};
139
+ const expected = trimmedNonEmptyString(args.expectedAssistantId);
140
+ const isForeign =
141
+ expected !== undefined &&
142
+ identity.assistantId !== undefined &&
143
+ identity.assistantId !== expected;
144
+
145
+ return { kind: isForeign ? "foreign" : "healthy", ...identity };
146
+ }
147
+
148
+ /**
149
+ * Fetch layers vary in how much of the request they echo into an error
150
+ * message, and the caller already knows which URL it asked about.
151
+ */
152
+ function unreachable(detail: string, base: string): TunnelProbeResult {
153
+ return {
154
+ kind: "unreachable",
155
+ detail: truncate(detail.replaceAll(base, REDACTED_URL), MAX_DETAIL_LENGTH),
156
+ };
157
+ }
158
+
159
+ /**
160
+ * An unread fetch body pins its socket until GC under Bun and undici, and the
161
+ * probe runs on every settings mount, resume, and manual refresh.
162
+ */
163
+ async function discardBody(response: Response | undefined): Promise<void> {
164
+ try {
165
+ await response?.body?.cancel();
166
+ } catch {
167
+ /* best effort */
168
+ }
169
+ }
170
+
171
+ function isTimeoutError(error: unknown): boolean {
172
+ return (
173
+ isPlainObject(error) &&
174
+ (error.name === "TimeoutError" || error.name === "AbortError")
175
+ );
176
+ }
177
+
178
+ function isGenericFetchMessage(message: string): boolean {
179
+ return GENERIC_FETCH_MESSAGES.has(message.toLowerCase());
180
+ }
181
+
182
+ /**
183
+ * A dead tunnel surfaces as a generic wrapper ("fetch failed") whose `cause`
184
+ * carries the reason worth reading (`ECONNREFUSED`, `ENOTFOUND`), so walk the
185
+ * chain for a code, then for the first message that says something.
186
+ */
187
+ function describeError(error: unknown): string {
188
+ let message: string | undefined;
189
+ let current: unknown = error;
190
+
191
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth += 1) {
192
+ if (!isPlainObject(current)) {
193
+ break;
194
+ }
195
+ if (isTimeoutError(current)) {
196
+ return "timeout";
197
+ }
198
+ const code = trimmedNonEmptyString(current.code);
199
+ if (code !== undefined) {
200
+ return code;
201
+ }
202
+ // A wrapper only holds the slot until a deeper level fills it.
203
+ if (message === undefined || isGenericFetchMessage(message)) {
204
+ message = trimmedNonEmptyString(current.message) ?? message;
205
+ }
206
+ current = current.cause;
207
+ }
208
+
209
+ return message ?? String(error);
210
+ }
211
+
212
+ interface ServedEdgeConfig {
213
+ assistantId?: string;
214
+ assistantName?: string;
215
+ }
216
+
217
+ /**
218
+ * What the config request settled: the identity the pairing edge reports, a
219
+ * positive "whatever is here does not serve the pairing app", or nothing.
220
+ */
221
+ type ServedConfigReading =
222
+ | { kind: "unanswered" }
223
+ | { kind: "notServed"; detail: string }
224
+ | { kind: "served"; config: ServedEdgeConfig };
225
+
226
+ /**
227
+ * A 2xx JSON object carrying the pairing edge's marker is that edge
228
+ * identifying itself, any other answer is an edge that does not serve the
229
+ * pairing app, and no answer is no evidence.
230
+ */
231
+ async function readServedConfig(
232
+ response: Response | undefined,
233
+ ): Promise<ServedConfigReading> {
234
+ if (response === undefined) {
235
+ return { kind: "unanswered" };
236
+ }
237
+ if (!response.ok) {
238
+ await discardBody(response);
239
+ return { kind: "notServed", detail: `HTTP ${response.status}` };
240
+ }
241
+ let detail = UNREADABLE_CONFIG_DETAIL;
242
+ try {
243
+ const body: unknown = await response.json();
244
+ if (isPlainObject(body)) {
245
+ if (body.mode === PAIRING_EDGE_MODE) {
246
+ return {
247
+ kind: "served",
248
+ config: {
249
+ assistantId: trimmedNonEmptyString(body.assistantId),
250
+ assistantName: trimmedNonEmptyString(body.assistantName),
251
+ },
252
+ };
253
+ }
254
+ detail = UNMARKED_CONFIG_DETAIL;
255
+ }
256
+ } catch {
257
+ /* an unparseable body is an edge answering with something else */
258
+ }
259
+ await discardBody(response);
260
+ return { kind: "notServed", detail };
261
+ }
@@ -1,10 +1,14 @@
1
1
  import { createServer } from "node:net";
2
2
  import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
3
4
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
5
 
5
6
  import { ensureSocketPathFree } from "../socket-cleanup.js";
6
7
  import { resolveIpcSocketPath } from "../socket-path.js";
7
8
 
9
+ // Fake workspace under tmpdir so the live-workspace guard accepts it.
10
+ const WS_DIR = join(tmpdir(), "vellum-workspace-test");
11
+
8
12
  let savedWorkspaceDir: string | undefined;
9
13
  let savedGatewayIpcSocketDir: string | undefined;
10
14
 
@@ -29,7 +33,7 @@ afterEach(() => {
29
33
  describe("resolveIpcSocketPath", () => {
30
34
  test("uses env var override when set", () => {
31
35
  process.env.GATEWAY_IPC_SOCKET_DIR = "/run/gateway-ipc";
32
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-workspace-test";
36
+ process.env.VELLUM_WORKSPACE_DIR = WS_DIR;
33
37
 
34
38
  const resolved = resolveIpcSocketPath("gateway");
35
39
 
@@ -39,28 +43,32 @@ describe("resolveIpcSocketPath", () => {
39
43
 
40
44
  test("ignores empty env var override", () => {
41
45
  process.env.GATEWAY_IPC_SOCKET_DIR = " ";
42
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-workspace-test";
46
+ process.env.VELLUM_WORKSPACE_DIR = WS_DIR;
43
47
 
44
48
  const resolved = resolveIpcSocketPath("gateway");
45
49
 
46
50
  expect(resolved.source).toBe("workspace");
47
- expect(resolved.path).toBe("/tmp/vellum-workspace-test/gateway.sock");
51
+ expect(resolved.path).toBe(join(WS_DIR, "gateway.sock"));
48
52
  });
49
53
 
50
54
  test("uses workspace path by default", () => {
51
55
  delete process.env.GATEWAY_IPC_SOCKET_DIR;
52
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-workspace-test";
56
+ process.env.VELLUM_WORKSPACE_DIR = WS_DIR;
53
57
 
54
58
  const resolved = resolveIpcSocketPath("gateway");
55
59
 
56
60
  expect(resolved.source).toBe("workspace");
57
- expect(resolved.path).toBe("/tmp/vellum-workspace-test/gateway.sock");
61
+ expect(resolved.path).toBe(join(WS_DIR, "gateway.sock"));
58
62
  });
59
63
 
60
64
  test("falls back to tmpdir when workspace path exceeds AF_UNIX limit", () => {
61
65
  delete process.env.GATEWAY_IPC_SOCKET_DIR;
62
66
  // 90-char workspace dir + /gateway.sock = well over 103 bytes
63
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/" + "a".repeat(85) + "/workspace";
67
+ process.env.VELLUM_WORKSPACE_DIR = join(
68
+ tmpdir(),
69
+ "a".repeat(90),
70
+ "workspace",
71
+ );
64
72
 
65
73
  const resolved = resolveIpcSocketPath("gateway");
66
74
 
@@ -70,7 +78,7 @@ describe("resolveIpcSocketPath", () => {
70
78
 
71
79
  test("derives a hyphenated socket name's env var and filename", () => {
72
80
  process.env.EXAMPLE_SERVICE_IPC_SOCKET_DIR = "/run/example-ipc";
73
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-workspace-test";
81
+ process.env.VELLUM_WORKSPACE_DIR = WS_DIR;
74
82
 
75
83
  const resolved = resolveIpcSocketPath("example-service");
76
84
 
@@ -143,7 +143,7 @@ describe("ipcClassifyRisk retry", () => {
143
143
  const CLASSIFY_OK = {
144
144
  risk: "low",
145
145
  reason: "safe",
146
- matchType: "none",
146
+ matchType: "registry",
147
147
  scopeOptions: [],
148
148
  };
149
149
  const emptyParams = {} as unknown as Parameters<typeof ipcClassifyRisk>[0];
@@ -10,16 +10,17 @@
10
10
  * deterministic fallback for long AF_UNIX paths.
11
11
  */
12
12
 
13
+ import {
14
+ type ClassifyRiskIpcParams,
15
+ type ClassifyRiskIpcResponse,
16
+ ClassifyRiskIpcResponseSchema,
17
+ } from "@vellumai/gateway-client";
13
18
  import {
14
19
  ipcCall as packageIpcCall,
15
20
  IpcCallError,
16
21
  PersistentIpcClient as PackagePersistentIpcClient,
17
22
  } from "@vellumai/gateway-client/ipc-client";
18
23
 
19
- import type {
20
- ClassificationResult,
21
- ClassifyRiskParams,
22
- } from "../permissions/ipc-risk-types.js";
23
24
  import { getLogger } from "../util/logger.js";
24
25
  import { abortableSleep, computeRetryDelay } from "../util/retry.js";
25
26
  import { resolveIpcSocketPath } from "./socket-path.js";
@@ -173,37 +174,29 @@ const CLASSIFY_RISK_ATTEMPT_TIMEOUT_MS = 1_500; // per-attempt cap (< 5s default
173
174
  * When a `signal` is supplied, retries stop as soon as it aborts.
174
175
  */
175
176
  export async function ipcClassifyRisk(
176
- params: ClassifyRiskParams,
177
+ params: ClassifyRiskIpcParams,
177
178
  signal?: AbortSignal,
178
- ): Promise<ClassificationResult | undefined> {
179
+ ): Promise<ClassifyRiskIpcResponse | undefined> {
179
180
  for (let attempt = 0; ; attempt++) {
180
181
  try {
181
182
  const result = await ipcCallPersistent(
182
183
  "classify_risk",
183
- params as unknown as Record<string, unknown>,
184
+ params,
184
185
  CLASSIFY_RISK_ATTEMPT_TIMEOUT_MS,
185
186
  );
186
187
 
187
- // Returned-but-malformed responses are deterministic, not transient:
188
+ // A returned-but-malformed response is deterministic, not transient:
188
189
  // fail closed immediately (no retry).
189
- if (!result || typeof result !== "object" || Array.isArray(result)) {
190
- log.warn(
191
- { result },
192
- "ipcClassifyRisk: gateway returned non-object response",
193
- );
194
- return undefined;
195
- }
196
-
197
- const obj = result as Record<string, unknown>;
198
- if (typeof obj.risk !== "string") {
190
+ const parsed = ClassifyRiskIpcResponseSchema.safeParse(result);
191
+ if (!parsed.success) {
199
192
  log.warn(
200
- { result },
201
- "ipcClassifyRisk: gateway response missing 'risk' field",
193
+ { result, issues: parsed.error.issues },
194
+ "ipcClassifyRisk: gateway response does not match the classify_risk contract",
202
195
  );
203
196
  return undefined;
204
197
  }
205
198
 
206
- return result as ClassificationResult;
199
+ return parsed.data;
207
200
  } catch (err) {
208
201
  // A structured gateway error means the gateway was reachable and
209
202
  // deterministically rejected the request — not a transient blip.
@@ -15,6 +15,8 @@ import {
15
15
  removeIpcEndpointFile,
16
16
  } from "@vellumai/ipc-server-utils";
17
17
 
18
+ import { makeAddrInUseError } from "../daemon/startup-error.js";
19
+
18
20
  /**
19
21
  * Maximum time to wait for the probe `connect()` to settle before declaring
20
22
  * the path occupied. Without a bound, a hung process holding the socket would
@@ -25,17 +27,6 @@ import {
25
27
  */
26
28
  const PROBE_CONNECT_TIMEOUT_MS = 2000;
27
29
 
28
- /**
29
- * Build an `EADDRINUSE`-coded error so callers (and `categorizeDaemonError`)
30
- * can branch on `err.code` and surface the structured "already running"
31
- * guidance instead of a generic UNKNOWN.
32
- */
33
- function makeAddrInUseError(message: string): NodeJS.ErrnoException {
34
- const err = new Error(message) as NodeJS.ErrnoException;
35
- err.code = "EADDRINUSE";
36
- return err;
37
- }
38
-
39
30
  /**
40
31
  * Probe-connect to `socketPath`. Behavior:
41
32
  * - Path doesn't exist → return.
@@ -67,7 +67,7 @@ mock.module("../../daemon/mcp-reload-service.js", () => ({
67
67
 
68
68
  mock.module("../../config/env-registry.js", () => ({
69
69
  getIsContainerized: () => false,
70
- getWorkspaceDirOverride: () => undefined,
70
+ getWorkspaceDirOverride: () => process.env.VELLUM_WORKSPACE_DIR,
71
71
  // Imported by the real util/logger.js; ESM named-import validation
72
72
  // requires it even though the silent test logger never calls it.
73
73
  getDebugStdoutLogs: () => false,
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+
3
+ import { CHANNEL_IDS } from "../channels/types.js";
4
+
5
+ /**
6
+ * What a stored channel message row knows about itself, in terms no single
7
+ * provider owns.
8
+ *
9
+ * "Provider" here is the channel sense, the one `messaging/providers/*` uses:
10
+ * Slack, Telegram, Discord. It is not the inference sense in `providers/*`,
11
+ * whose `providerMetadata` rides on a content block and describes a model.
12
+ *
13
+ * Named for the provider rather than the channel because `channel metadata`
14
+ * is already taken: `buildChannelMetadata` builds the whole envelope a
15
+ * message row carries (provenance, `userMessageChannel`, interfaces), and
16
+ * this is one key inside that envelope. This is the neutral counterpart of
17
+ * `slackMeta`, so it reads as the generalization of what `providers/slack`
18
+ * stores.
19
+ *
20
+ * Every channel needs the same facts to reconstruct a transcript: which chat
21
+ * the row belongs to, the provider's id for the row itself, the thread it sits
22
+ * in, whether it is a message or a reaction, and, when it is a reaction, which
23
+ * message it was attached to.
24
+ *
25
+ * History assembly is structural, not Slack logic: it groups a reaction with
26
+ * the message it targets, inside a thread. That rule holds for any
27
+ * thread-scoped channel.
28
+ *
29
+ * The schema passes through what it does not name, so a provider carries its
30
+ * own fields (Slack's file markers and timezone labels) on this same object
31
+ * and validates them with its own schema. This is how `SourceMetadataSchema`
32
+ * already carries `slackBotMentioned` and the email fields on the wire: one
33
+ * object per row, no per-provider sub-envelope, and no second copy of
34
+ * anything named here.
35
+ */
36
+
37
+ const providerReactionMetadataSchema = z.object({
38
+ /**
39
+ * Provider id of the message this reaction was attached to, in the same
40
+ * namespace as `messageId`. Resolution is keyed on it, so it is required.
41
+ */
42
+ targetMessageId: z.string(),
43
+ emoji: z.string(),
44
+ op: z.enum(["added", "removed"]),
45
+ actorDisplayName: z.string().optional(),
46
+ });
47
+
48
+ export const providerMessageMetadataSchema = z
49
+ .object({
50
+ source: z.enum(CHANNEL_IDS),
51
+ /**
52
+ * Provider id of the chat, channel or room this row belongs to. Same name
53
+ * and meaning as on the inbound wire, where "conversation" is the canonical
54
+ * word for a delivery address and "channel" names the provider.
55
+ */
56
+ conversationExternalId: z.string(),
57
+ /**
58
+ * Provider id of this row itself. Absent on a reaction, which no channel
59
+ * gives an id of its own: a reaction event names the chat, the message it
60
+ * was attached to, the actor and the emoji, and nothing identifies the
61
+ * reaction. The message it acts on is `reaction.targetMessageId`.
62
+ */
63
+ messageId: z.string().optional(),
64
+ /**
65
+ * Provider id of the thread this row sits in, absent when it is not in one.
66
+ * Never synthesized from `messageId`: a value here asserts that a thread
67
+ * exists, and inventing one keys conversations on threads that never do.
68
+ */
69
+ threadId: z.string().optional(),
70
+ /**
71
+ * Provider id of whoever authored the row or performed the reaction. Trust
72
+ * is keyed on this everywhere else, and it is what lets a channel say who
73
+ * acted; a display name is a label, not an identity. Optional because a
74
+ * provider envelope need not carry one.
75
+ */
76
+ actorExternalId: z.string().optional(),
77
+ displayName: z.string().optional(),
78
+ /**
79
+ * Only reactions get a row of their own. An edit rewrites the row it
80
+ * targets and a delete stamps `deletedAt` on it, so neither appears here.
81
+ */
82
+ eventKind: z.enum(["message", "reaction"]),
83
+ reaction: providerReactionMetadataSchema.optional(),
84
+ editedAt: z.number().optional(),
85
+ deletedAt: z.number().optional(),
86
+ })
87
+ .passthrough();
88
+
89
+ export type ProviderReactionMetadata = z.infer<
90
+ typeof providerReactionMetadataSchema
91
+ >;
92
+ export type ProviderMessageMetadata = z.infer<
93
+ typeof providerMessageMetadataSchema
94
+ >;
95
+
96
+ /**
97
+ * Parse and validate a serialized `ProviderMessageMetadata`, the counterpart of
98
+ * `readSlackMetadata` for the neutral shape. Anything that does not parse or
99
+ * does not validate reads as null.
100
+ */
101
+ export function readProviderMessageMetadata(
102
+ raw: unknown,
103
+ ): ProviderMessageMetadata | null {
104
+ if (typeof raw !== "string") {
105
+ return null;
106
+ }
107
+ let parsed: unknown;
108
+ try {
109
+ parsed = JSON.parse(raw);
110
+ } catch {
111
+ return null;
112
+ }
113
+ const result = providerMessageMetadataSchema.safeParse(parsed);
114
+ return result.success ? result.data : null;
115
+ }
116
+
117
+ /**
118
+ * The id a row is grouped by when assembling a thread: its own for a message,
119
+ * its target's for a reaction, so a reaction lands beside the message it was
120
+ * attached to rather than in a block of its own.
121
+ *
122
+ * Undefined for a row that identifies neither, which cannot be placed.
123
+ */
124
+ export function groupingMessageId(
125
+ meta: ProviderMessageMetadata,
126
+ ): string | undefined {
127
+ return meta.reaction?.targetMessageId ?? meta.messageId;
128
+ }