@vellumai/assistant 0.11.4 → 0.11.5-staging.1

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 (358) hide show
  1. package/AGENTS.md +2 -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__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/conversation-attachments.test.ts +0 -1
  38. package/src/__tests__/conversation-error.test.ts +17 -0
  39. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  40. package/src/__tests__/conversation-pairing.test.ts +186 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  42. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  43. package/src/__tests__/credential-routes.test.ts +22 -3
  44. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  45. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  46. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  47. package/src/__tests__/dm-backfill.test.ts +63 -0
  48. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  49. package/src/__tests__/edit-propagation.test.ts +107 -4
  50. package/src/__tests__/events-client-registration.test.ts +28 -0
  51. package/src/__tests__/file-write-tool.test.ts +4 -2
  52. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  53. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  54. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  55. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  56. package/src/__tests__/host-shell-tool.test.ts +23 -4
  57. package/src/__tests__/image-conversion.test.ts +143 -1
  58. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  59. package/src/__tests__/input-repairs.test.ts +207 -0
  60. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  61. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  62. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  63. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  64. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  65. package/src/__tests__/mtime-cache.test.ts +2 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  67. package/src/__tests__/openai-provider.test.ts +22 -0
  68. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  69. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  70. package/src/__tests__/platform.test.ts +16 -1
  71. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  72. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  73. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  74. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  75. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  76. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  77. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  78. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  79. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  80. package/src/__tests__/reaction-persistence.test.ts +200 -7
  81. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  82. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  83. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  84. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  85. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  86. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  87. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  88. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  89. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  90. package/src/__tests__/tool-executor.test.ts +63 -38
  91. package/src/__tests__/tool-policy.test.ts +97 -1
  92. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  93. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  94. package/src/__tests__/validate-input.test.ts +243 -7
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  97. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  98. package/src/acp/acp-claude-oauth.ts +38 -21
  99. package/src/acp/prepare-agent-env.ts +146 -55
  100. package/src/agent/loop-tool-dedup.test.ts +161 -0
  101. package/src/agent/loop.ts +68 -22
  102. package/src/api/constants/app-tools.ts +34 -0
  103. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  104. package/src/api/index.ts +19 -0
  105. package/src/api/responses/resource-pressure-status.ts +25 -0
  106. package/src/approvals/guardian-channel-delivery.ts +70 -6
  107. package/src/approvals/guardian-request-resolvers.ts +32 -66
  108. package/src/channels/__tests__/message-audience.test.ts +49 -0
  109. package/src/channels/__tests__/types.test.ts +17 -3
  110. package/src/channels/message-audience.ts +40 -0
  111. package/src/channels/types.ts +16 -12
  112. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  113. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  114. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  115. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  116. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  117. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  118. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  119. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  120. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  121. package/src/cli/commands/conversations.help.ts +34 -0
  122. package/src/cli/commands/conversations.ts +125 -0
  123. package/src/cli/commands/inference-providers.ts +9 -4
  124. package/src/cli/commands/inference.help.ts +9 -4
  125. package/src/cli/commands/keys.help.ts +13 -1
  126. package/src/cli/commands/keys.ts +26 -15
  127. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  128. package/src/cli/commands/memory/index.help.ts +41 -27
  129. package/src/cli/commands/memory/index.ts +2 -0
  130. package/src/cli/commands/memory/memory-v2.ts +58 -54
  131. package/src/cli/commands/memory/memory-validate.ts +18 -0
  132. package/src/cli/commands/monitoring.ts +1 -0
  133. package/src/cli/commands/plugins.help.ts +28 -31
  134. package/src/cli/commands/plugins.ts +10 -0
  135. package/src/cli/commands/skills.help.ts +13 -16
  136. package/src/cli/commands/trust.ts +3 -14
  137. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  138. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  139. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  140. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  141. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  142. package/src/cli/lib/bundled-marketplace.json +13 -0
  143. package/src/cli/lib/daemon-credential-client.ts +25 -3
  144. package/src/cli/lib/install-from-github.ts +35 -24
  145. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  146. package/src/cli/lib/plugin-surfaces.ts +27 -0
  147. package/src/config/feature-flag-registry.json +35 -19
  148. package/src/config/webhook-routing.ts +8 -0
  149. package/src/context/compactor.ts +31 -2
  150. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  151. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  152. package/src/daemon/conversation-error.ts +13 -0
  153. package/src/daemon/conversation-messaging.ts +22 -2
  154. package/src/daemon/conversation-process.ts +11 -0
  155. package/src/daemon/conversation-store.ts +10 -0
  156. package/src/daemon/conversation-surfaces.ts +21 -8
  157. package/src/daemon/conversation.ts +12 -10
  158. package/src/daemon/lifecycle.ts +6 -2
  159. package/src/daemon/message-types/conversations.ts +5 -7
  160. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  161. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  162. package/src/daemon/resource-pressure-guard.ts +387 -0
  163. package/src/daemon/shutdown-handlers.ts +2 -0
  164. package/src/daemon/startup-error.ts +16 -0
  165. package/src/daemon/trust-context.ts +14 -10
  166. package/src/daemon/unsendable-image-notice.ts +76 -0
  167. package/src/hooks/registry.ts +65 -51
  168. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  169. package/src/ipc/gateway-client.test.ts +1 -1
  170. package/src/ipc/gateway-client.ts +14 -21
  171. package/src/ipc/socket-cleanup.ts +2 -11
  172. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  173. package/src/messaging/provider-message-metadata.ts +128 -0
  174. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  175. package/src/messaging/providers/channel-transport.ts +90 -13
  176. package/src/messaging/providers/discord/send.ts +16 -0
  177. package/src/messaging/providers/discord/transport.ts +11 -1
  178. package/src/messaging/providers/index.ts +109 -16
  179. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  180. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  181. package/src/messaging/providers/slack/send.test.ts +6 -9
  182. package/src/messaging/providers/slack/send.ts +40 -28
  183. package/src/messaging/providers/slack/transport.ts +22 -26
  184. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  185. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  186. package/src/messaging/read-provider-metadata.test.ts +157 -0
  187. package/src/messaging/read-provider-metadata.ts +51 -0
  188. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  189. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  190. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  191. package/src/notifications/adapters/macos.ts +55 -0
  192. package/src/notifications/adapters/slack.ts +10 -5
  193. package/src/notifications/assistant-reply-producer.ts +37 -4
  194. package/src/notifications/conversation-pairing.ts +111 -21
  195. package/src/notifications/edit-notification.ts +38 -8
  196. package/src/notifications/emit-signal.ts +12 -14
  197. package/src/notifications/home-feed-side-effect.ts +218 -15
  198. package/src/notifications/types.ts +5 -0
  199. package/src/permissions/AGENTS.md +16 -0
  200. package/src/permissions/checker.test.ts +75 -122
  201. package/src/permissions/checker.ts +44 -560
  202. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  203. package/src/permissions/confirmation-guardian-request.ts +5 -1
  204. package/src/persistence/conversation-crud.ts +23 -20
  205. package/src/persistence/conversation-queries.ts +14 -1
  206. package/src/persistence/conversation-types.ts +22 -0
  207. package/src/persistence/delivery-crud.ts +122 -15
  208. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  209. package/src/plugin-api/conversation-turn.ts +27 -0
  210. package/src/plugin-api/credential-scope.test.ts +15 -0
  211. package/src/plugin-api/credential-scope.ts +14 -0
  212. package/src/plugin-api/index.ts +19 -1
  213. package/src/plugin-api/resolve-credential.ts +15 -10
  214. package/src/plugin-api/store-credential.ts +148 -0
  215. package/src/plugin-api/system-card.ts +36 -0
  216. package/src/plugin-api/vision-support.test.ts +82 -0
  217. package/src/plugin-api/vision-support.ts +14 -2
  218. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  219. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  220. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  221. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  222. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  223. package/src/plugins/defaults/main.ts +15 -0
  224. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  225. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  226. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  227. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  228. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  229. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  230. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  231. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  232. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  233. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  234. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  238. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  239. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  240. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  241. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  242. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  243. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  244. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  245. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  246. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  247. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  248. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  249. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  250. package/src/plugins/mtime-cache.ts +1 -1
  251. package/src/plugins/pipeline.ts +14 -7
  252. package/src/plugins/plugin-execution-context.ts +35 -11
  253. package/src/plugins/plugin-tree-walk.ts +63 -4
  254. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  255. package/src/providers/anthropic/client.ts +332 -241
  256. package/src/providers/connection-resolution.ts +2 -1
  257. package/src/providers/content-blocks.ts +22 -0
  258. package/src/providers/gemini/client.ts +5 -2
  259. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  260. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  261. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  262. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  263. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  264. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  265. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  266. package/src/providers/inference/adapter-factory.ts +33 -2
  267. package/src/providers/inference/auth.ts +13 -2
  268. package/src/providers/inference/credential-usage.ts +37 -0
  269. package/src/providers/inference/missing-credential-guard.ts +110 -0
  270. package/src/providers/inference/resolve-auth.ts +7 -7
  271. package/src/providers/media-resolve.ts +176 -15
  272. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  273. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  274. package/src/providers/openai/chat-completions-provider.ts +322 -42
  275. package/src/routes/route-host-protocol.ts +7 -0
  276. package/src/routes/worker.ts +31 -10
  277. package/src/runtime/AGENTS.md +35 -0
  278. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  279. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  280. package/src/runtime/assistant-event-hub.ts +38 -0
  281. package/src/runtime/channel-reply-delivery.ts +54 -31
  282. package/src/runtime/channel-retry-sweep.ts +6 -4
  283. package/src/runtime/effective-capabilities.test.ts +19 -0
  284. package/src/runtime/effective-capabilities.ts +25 -0
  285. package/src/runtime/guardian-reply-router.ts +6 -2
  286. package/src/runtime/http-errors.ts +1 -0
  287. package/src/runtime/http-server.ts +20 -5
  288. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  289. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  290. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  291. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  292. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  293. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  294. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  295. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  296. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  297. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  298. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  299. package/src/runtime/routes/approval-routes.ts +37 -4
  300. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  301. package/src/runtime/routes/canned-message-complete.ts +68 -29
  302. package/src/runtime/routes/client-routes.ts +113 -0
  303. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  304. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  305. package/src/runtime/routes/conversation-routes.ts +36 -22
  306. package/src/runtime/routes/credential-in-use.ts +85 -0
  307. package/src/runtime/routes/credential-routes.ts +55 -85
  308. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  309. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  310. package/src/runtime/routes/identity-routes.ts +5 -121
  311. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  312. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  313. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  314. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  315. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  316. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  317. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  318. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  319. package/src/runtime/routes/index.ts +2 -0
  320. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  321. package/src/runtime/routes/monitoring-routes.ts +9 -1
  322. package/src/runtime/routes/plugins-routes.ts +17 -5
  323. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  324. package/src/runtime/routes/secret-routes.ts +39 -8
  325. package/src/runtime/routes/settings-routes.ts +6 -6
  326. package/src/runtime/routes/surface-action-routes.ts +20 -19
  327. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  328. package/src/runtime/routes/user-route-resolution.ts +18 -1
  329. package/src/runtime/slack-reply-session.test.ts +23 -13
  330. package/src/runtime/slack-reply-session.ts +38 -55
  331. package/src/runtime/web-presence.ts +90 -0
  332. package/src/skills/validate-input.ts +177 -40
  333. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  334. package/src/subagent/consult-context.ts +7 -23
  335. package/src/tools/credentials/broker.ts +24 -79
  336. package/src/tools/credentials/ref-parse.ts +35 -0
  337. package/src/tools/credentials/resolve.ts +4 -10
  338. package/src/tools/credentials/store.ts +168 -0
  339. package/src/tools/credentials/tool-policy.ts +67 -0
  340. package/src/tools/executor.ts +53 -15
  341. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  342. package/src/tools/network/url-safety.ts +9 -16
  343. package/src/tools/network/web-search.ts +21 -3
  344. package/src/tools/permission-checker.ts +43 -52
  345. package/src/tools/schema-transforms.ts +40 -5
  346. package/src/tools/shared/input-repairs.ts +160 -0
  347. package/src/tools/skills/skill-tool-factory.ts +18 -4
  348. package/src/tools/subagent/spawn.ts +0 -1
  349. package/src/tools/tool-approval-handler.ts +16 -10
  350. package/src/tools/tool-types.ts +26 -13
  351. package/src/tools/types.ts +6 -6
  352. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  353. package/src/util/cgroup-memory.ts +3 -0
  354. package/src/util/container-cpu-sampler.ts +250 -0
  355. package/src/util/image-conversion.ts +178 -14
  356. package/src/util/platform.ts +93 -10
  357. package/src/permissions/ipc-risk-types.ts +0 -143
  358. package/src/permissions/risk-types.ts +0 -76
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The app tools that mutate the stored app set, and what each one leaves
3
+ * behind. Mirrors `document-tools.ts`: a single declaration both the daemon and
4
+ * the web client derive their tool sets from, so a new mutating app tool is
5
+ * added here once and every consumer picks it up.
6
+ *
7
+ * Tool names are the ones the runner sees. Calls arriving through the bundled
8
+ * `app-builder` skill reach the daemon already unwrapped to the inner tool
9
+ * name, and the web client unwraps the `skill_execute` envelope itself.
10
+ */
11
+
12
+ export interface AppMutationTool {
13
+ /** The executed tool name. */
14
+ readonly name: string;
15
+ /**
16
+ * Whether the app is still openable once the tool has run. A deleted app has
17
+ * nothing to open, so it cannot anchor a card back to itself.
18
+ */
19
+ readonly reopenable: boolean;
20
+ }
21
+
22
+ export const APP_MUTATION_TOOLS: readonly AppMutationTool[] = [
23
+ { name: "app_create", reopenable: true },
24
+ { name: "app_update", reopenable: true },
25
+ { name: "app_delete", reopenable: false },
26
+ ];
27
+
28
+ /**
29
+ * The mutating app tools that leave the app openable. The web transcript
30
+ * anchors its end-of-response app cards on these, so a delete never produces a
31
+ * card pointing at an app that is gone.
32
+ */
33
+ export const APP_MUTATION_TOOL_NAMES: readonly string[] =
34
+ APP_MUTATION_TOOLS.filter((tool) => tool.reopenable).map((tool) => tool.name);
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `resource_pressure_status_changed` SSE event.
3
+ *
4
+ * Global broadcast pushed when the daemon's resource-pressure snapshot
5
+ * changes. Mirrors the REST `GET /resource-pressure/status` shape so the
6
+ * client's monitor can apply stream updates and polled fetches through
7
+ * the same code path. Carries no `conversationId`: it is workspace-wide.
8
+ *
9
+ * Canonical wire-contract source. Daemon code imports the type
10
+ * directly from this file; external consumers import via
11
+ * `@vellumai/assistant-api`.
12
+ */
13
+
14
+ import { z } from "zod";
15
+
16
+ export const ResourcePressureStateSchema = z.enum([
17
+ "disabled",
18
+ "ok",
19
+ "elevated",
20
+ "unknown",
21
+ ]);
22
+
23
+ export type ResourcePressureState = z.infer<typeof ResourcePressureStateSchema>;
24
+
25
+ export const ResourcePressureStatusSchema = z.object({
26
+ enabled: z.boolean(),
27
+ state: ResourcePressureStateSchema,
28
+ /** Latest CPU sample as a percent of the CPU allocation. */
29
+ cpuPercent: z.number().nullable(),
30
+ /** Working set as a percent of the memory limit. */
31
+ memoryPercent: z.number().nullable(),
32
+ /** Whether the CPU signal is holding the `elevated` state. */
33
+ cpuElevated: z.boolean(),
34
+ /** Whether the memory signal is holding the `elevated` state. */
35
+ memoryElevated: z.boolean(),
36
+ cpuThresholdPercent: z.number(),
37
+ memoryThresholdPercent: z.number(),
38
+ lastCheckedAt: z.string().nullable(),
39
+ error: z.string().nullable(),
40
+ });
41
+
42
+ export type ResourcePressureStatus = z.infer<
43
+ typeof ResourcePressureStatusSchema
44
+ >;
45
+
46
+ export const ResourcePressureStatusChangedEventSchema = z.object({
47
+ type: z.literal("resource_pressure_status_changed"),
48
+ status: ResourcePressureStatusSchema,
49
+ });
50
+
51
+ export type ResourcePressureStatusChangedEvent = z.infer<
52
+ typeof ResourcePressureStatusChangedEventSchema
53
+ >;
package/src/api/index.ts CHANGED
@@ -102,6 +102,7 @@ import {
102
102
  RecordingStopEventSchema,
103
103
  } from "./events/recording.js";
104
104
  import { RelationshipStateUpdatedEventSchema } from "./events/relationship-state-updated.js";
105
+ import { ResourcePressureStatusChangedEventSchema } from "./events/resource-pressure-status-changed.js";
105
106
  import { ScheduleConversationCreatedEventSchema } from "./events/schedule-conversation-created.js";
106
107
  import { SecretRequestEventSchema } from "./events/secret-request.js";
107
108
  import { ServiceGroupUpdateCompleteEventSchema } from "./events/service-group-update-complete.js";
@@ -134,6 +135,11 @@ import { WorkflowLeafStartedEventSchema } from "./events/workflow-leaf-started.j
134
135
  import { WorkflowProgressEventSchema } from "./events/workflow-progress.js";
135
136
  import { WorkflowStartedEventSchema } from "./events/workflow-started.js";
136
137
 
138
+ export {
139
+ APP_MUTATION_TOOL_NAMES,
140
+ APP_MUTATION_TOOLS,
141
+ type AppMutationTool,
142
+ } from "./constants/app-tools.js";
137
143
  export {
138
144
  CALL_SITE_COMPACTION_AGENT,
139
145
  CALL_SITE_SYNTHETIC_AGENT_ERROR_MESSAGE,
@@ -536,6 +542,14 @@ export {
536
542
  type RelationshipStateUpdatedEvent,
537
543
  RelationshipStateUpdatedEventSchema,
538
544
  } from "./events/relationship-state-updated.js";
545
+ export {
546
+ type ResourcePressureState,
547
+ ResourcePressureStateSchema,
548
+ type ResourcePressureStatus,
549
+ type ResourcePressureStatusChangedEvent,
550
+ ResourcePressureStatusChangedEventSchema,
551
+ ResourcePressureStatusSchema,
552
+ } from "./events/resource-pressure-status-changed.js";
539
553
  export {
540
554
  type ScheduleConversationCreatedEvent,
541
555
  ScheduleConversationCreatedEventSchema,
@@ -802,6 +816,10 @@ export {
802
816
  type MemoryV3SelectionRow,
803
817
  MemoryV3SelectionRowSchema,
804
818
  } from "./responses/memory-v3-selection-log.js";
819
+ export {
820
+ type ResourcePressureStatusResponse,
821
+ ResourcePressureStatusResponseSchema,
822
+ } from "./responses/resource-pressure-status.js";
805
823
  export {
806
824
  type SubagentDetailEvent,
807
825
  SubagentDetailEventSchema,
@@ -987,6 +1005,7 @@ export const AssistantEventSchema = z.discriminatedUnion("type", [
987
1005
  RecordingStartEventSchema,
988
1006
  RecordingStopEventSchema,
989
1007
  RelationshipStateUpdatedEventSchema,
1008
+ ResourcePressureStatusChangedEventSchema,
990
1009
  ScheduleConversationCreatedEventSchema,
991
1010
  SecretRequestEventSchema,
992
1011
  ServiceGroupUpdateCompleteEventSchema,
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Wire contract for the resource-pressure REST endpoint
3
+ * (`GET /resource-pressure/status`). Returns the current snapshot
4
+ * wrapped as `{ status }`.
5
+ *
6
+ * Reuses the canonical `ResourcePressureStatusSchema` defined alongside
7
+ * the `resource_pressure_status_changed` SSE event so the polled REST
8
+ * fetch and the streamed update share a single shape.
9
+ *
10
+ * Canonical wire-contract source. Assistant code imports the types
11
+ * directly from this file via relative paths; external consumers
12
+ * (web client, gateway, evals) import via `@vellumai/assistant-api`.
13
+ */
14
+
15
+ import { z } from "zod";
16
+
17
+ import { ResourcePressureStatusSchema } from "../events/resource-pressure-status-changed.js";
18
+
19
+ export const ResourcePressureStatusResponseSchema = z.object({
20
+ status: ResourcePressureStatusSchema,
21
+ });
22
+
23
+ export type ResourcePressureStatusResponse = z.infer<
24
+ typeof ResourcePressureStatusResponseSchema
25
+ >;
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Shared addressing helpers for guardian requester-facing channel notices.
2
+ * Shared addressing helpers for guardian-flow channel notices.
3
3
  *
4
- * Requester notices (approval, denial, expiry) are delivered straight to the
5
- * requester's chat via `deliverChannelReply` — independent of the
6
- * guardian-facing notification pipeline. Centralizing the addressing rules here
7
- * keeps the decision resolvers and the timer-driven expiry sweep from drifting
8
- * apart on how a requester is reached.
4
+ * Requester notices (approval, denial, expiry) and the guardian's own approval
5
+ * prompt are delivered straight to a chat via `deliverChannelReply` -
6
+ * independent of the guardian-facing notification pipeline. Centralizing the
7
+ * addressing rules here keeps the decision resolvers, the timer-driven expiry
8
+ * sweep and the in-turn approval prompt from drifting apart on who a message
9
+ * is put in front of.
9
10
  */
10
11
 
11
12
  /**
@@ -111,3 +112,66 @@ export function resolveRequesterDeliveryTarget(params: {
111
112
  }
112
113
  return requesterChatId;
113
114
  }
115
+
116
+ /**
117
+ * Reduce a reply callback to its channel route, dropping every query param.
118
+ *
119
+ * A callback addresses the turn it came from. The gateway hangs the turn's
120
+ * coordinates on it as params, and each transport reads a different one:
121
+ * Slack a `threadTs`, Telegram a forum topic `threadId`, Discord a `threadId`
122
+ * that replaces the destination outright rather than narrowing it. Carried
123
+ * onto a delivery aimed elsewhere they either fail the send or quietly win
124
+ * over the new address, and a redirect that leaves one behind is
125
+ * indistinguishable from no redirect at all.
126
+ *
127
+ * Everything is dropped rather than a named set, so a channel added later, or
128
+ * a param a transport starts reading later, cannot silently escape this. The
129
+ * channel itself survives because it is the path, which is also what
130
+ * `isDirectDelivery` resolves a transport from, and nothing the gateway hangs
131
+ * on a deliver callback is needed to authorize or route the send.
132
+ *
133
+ * Relative or malformed URLs are returned as-is; they carry no params.
134
+ */
135
+ export function stripTurnDestination(replyCallbackUrl: string): string {
136
+ try {
137
+ const url = new URL(replyCallbackUrl);
138
+ url.search = "";
139
+ return url.toString();
140
+ } catch {
141
+ return replyCallbackUrl;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Resolve where a guardian's own approval prompt is delivered.
147
+ *
148
+ * The prompt is raised by a turn the guardian is having, and that turn can be
149
+ * running in a room: a shared Slack channel, a Telegram group. The card
150
+ * carries the tool name and a preview of the command, so delivering it where
151
+ * the turn is shows both to everyone there.
152
+ *
153
+ * It goes to the guardian's own bound chat instead, the address they
154
+ * nominated when they verified and the one the notification pipeline already
155
+ * sends guardian cards to. No channel is named in this rule and none can be
156
+ * forgotten by it: a bound chat is the guardian's by definition, on any
157
+ * channel, including ones not built yet.
158
+ *
159
+ * A bound chat equal to the turn's chat means the turn is already there, so
160
+ * nothing moves. That also covers a turn where no binding resolved, since the
161
+ * trust context falls back to the turn's own chat, leaving delivery exactly as
162
+ * it is rather than dropping it.
163
+ */
164
+ export function resolveGuardianPromptDelivery(params: {
165
+ turnChatId: string;
166
+ turnCallbackUrl: string;
167
+ guardianChatId: string | undefined;
168
+ }): { chatId: string; callbackUrl: string } {
169
+ const { turnChatId, turnCallbackUrl, guardianChatId } = params;
170
+ if (!guardianChatId || guardianChatId === turnChatId) {
171
+ return { chatId: turnChatId, callbackUrl: turnCallbackUrl };
172
+ }
173
+ return {
174
+ chatId: guardianChatId,
175
+ callbackUrl: stripTurnDestination(turnCallbackUrl),
176
+ };
177
+ }
@@ -23,8 +23,10 @@ import type {
23
23
  GuardianRequestWire,
24
24
  } from "../channels/gateway-guardian-requests.js";
25
25
  import { getGuardianRequestOrNull } from "../channels/gateway-guardian-requests.js";
26
+ import { audienceForReader } from "../channels/message-audience.js";
26
27
  import { findContactChannel } from "../contacts/contact-store.js";
27
28
  import { findConversation } from "../daemon/conversation-registry.js";
29
+ import { isSlackDmConversation } from "../messaging/providers/slack/message-metadata.js";
28
30
  import { emitNotificationSignal } from "../notifications/emit-signal.js";
29
31
  import { parseQuestionAnswerActionId } from "../notifications/guardian-question-mode.js";
30
32
  import {
@@ -62,6 +64,7 @@ import {
62
64
  channelDeliversToUserId,
63
65
  resolveDeliverCallbackUrlForChannel,
64
66
  resolveRequesterDeliveryTarget,
67
+ stripTurnDestination,
65
68
  } from "./guardian-channel-delivery.js";
66
69
 
67
70
  const log = getLogger("guardian-request-resolvers");
@@ -70,38 +73,6 @@ const log = getLogger("guardian-request-resolvers");
70
73
  // Helpers
71
74
  // ---------------------------------------------------------------------------
72
75
 
73
- /**
74
- * Determines whether a Slack delivery should use ephemeral mode.
75
- *
76
- * Ephemeral messages (`chat.postEphemeral`) require a real channel ID
77
- * (starts with `C` for public/private channels, or `D` for DM conversations).
78
- * When the chat ID is a user ID (starts with `U`), `chat.postEphemeral` fails
79
- * with `channel_not_found`. In that case the message is already going to a DM
80
- * opened by `chat.postMessage`, so ephemeral isn't needed.
81
- *
82
- * Returns `true` only when the source channel is Slack AND the chatId is a
83
- * shared channel (starts with `C`), meaning other users could see the message.
84
- */
85
- function shouldUseEphemeral(sourceChannel: string, chatId: string): boolean {
86
- return sourceChannel === "slack" && chatId.startsWith("C");
87
- }
88
-
89
- /**
90
- * Strip the `threadTs` query param from a reply callback URL. The param
91
- * addresses the guardian's channel thread; reusing it for a DM delivery
92
- * raises `thread_not_found`. Relative or malformed URLs are returned as-is —
93
- * they carry no threadTs to strip.
94
- */
95
- function stripThreadTsParam(replyCallbackUrl: string): string {
96
- try {
97
- const url = new URL(replyCallbackUrl);
98
- url.searchParams.delete("threadTs");
99
- return url.toString();
100
- } catch {
101
- return replyCallbackUrl;
102
- }
103
- }
104
-
105
76
  /**
106
77
  * Deliver the verification code straight to the requester's DM so the
107
78
  * guardian is never an out-of-band courier for the secret.
@@ -125,7 +96,7 @@ async function deliverVerificationCodeToRequester(params: {
125
96
  verificationCode: string;
126
97
  assistantId: string;
127
98
  }): Promise<boolean> {
128
- const callbackUrl = stripThreadTsParam(params.replyCallbackUrl);
99
+ const callbackUrl = stripTurnDestination(params.replyCallbackUrl);
129
100
 
130
101
  try {
131
102
  await deliverChannelReply(callbackUrl, {
@@ -194,13 +165,13 @@ function buildRequesterChannelNotice(params: {
194
165
  text: params.text,
195
166
  assistantId: params.assistantId,
196
167
  };
197
- if (
198
- shouldUseEphemeral(params.channel, params.requesterChatId) &&
199
- params.requesterExternalUserId
200
- ) {
201
- payload.ephemeral = true;
202
- payload.user = params.requesterExternalUserId;
203
- }
168
+ payload.audience = audienceForReader(
169
+ params.channel,
170
+
171
+ params.requesterChatId,
172
+
173
+ params.requesterExternalUserId,
174
+ );
204
175
  return payload;
205
176
  }
206
177
 
@@ -1309,16 +1280,11 @@ const accessRequestResolver: GuardianRequestResolver = {
1309
1280
  };
1310
1281
  // On Slack shared channels, deliver the verification code as ephemeral
1311
1282
  // so only the guardian sees the secret — not all channel members.
1312
- if (
1313
- shouldUseEphemeral(
1314
- ctx.actor.channel,
1315
- guardianInBandContext.guardianChatId,
1316
- ) &&
1317
- ctx.actor.actorExternalUserId
1318
- ) {
1319
- codePayload.ephemeral = true;
1320
- codePayload.user = ctx.actor.actorExternalUserId;
1321
- }
1283
+ codePayload.audience = audienceForReader(
1284
+ ctx.actor.channel,
1285
+ guardianInBandContext.guardianChatId,
1286
+ ctx.actor.actorExternalUserId,
1287
+ );
1322
1288
  await deliverChannelReply(
1323
1289
  guardianInBandContext.replyCallbackUrl,
1324
1290
  codePayload,
@@ -1339,9 +1305,9 @@ const accessRequestResolver: GuardianRequestResolver = {
1339
1305
  codeDelivered &&
1340
1306
  ctx.actor.channel === "slack" &&
1341
1307
  guardianUserId &&
1342
- !guardianInBandContext.guardianChatId.startsWith("D")
1308
+ !isSlackDmConversation(guardianInBandContext.guardianChatId)
1343
1309
  ) {
1344
- const dmCallbackUrl = stripThreadTsParam(
1310
+ const dmCallbackUrl = stripTurnDestination(
1345
1311
  guardianInBandContext.replyCallbackUrl,
1346
1312
  );
1347
1313
 
@@ -1543,13 +1509,13 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
1543
1509
  text: `Your request to use "${request.toolName}" has been denied by the guardian.`,
1544
1510
  assistantId,
1545
1511
  };
1546
- if (
1547
- shouldUseEphemeral(request.sourceChannel ?? "", requesterChatId) &&
1548
- request.requesterExternalUserId
1549
- ) {
1550
- grantDenialPayload.ephemeral = true;
1551
- grantDenialPayload.user = request.requesterExternalUserId;
1552
- }
1512
+ grantDenialPayload.audience = audienceForReader(
1513
+ request.sourceChannel,
1514
+
1515
+ requesterChatId,
1516
+
1517
+ request.requesterExternalUserId,
1518
+ );
1553
1519
  await deliverChannelReply(
1554
1520
  channelDeliveryContext.replyCallbackUrl,
1555
1521
  grantDenialPayload,
@@ -1642,13 +1608,13 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
1642
1608
  text: `Your request to use "${request.toolName}" has been approved. Please retry your request.`,
1643
1609
  assistantId,
1644
1610
  };
1645
- if (
1646
- shouldUseEphemeral(request.sourceChannel ?? "", requesterChatId) &&
1647
- request.requesterExternalUserId
1648
- ) {
1649
- grantApprovalPayload.ephemeral = true;
1650
- grantApprovalPayload.user = request.requesterExternalUserId;
1651
- }
1611
+ grantApprovalPayload.audience = audienceForReader(
1612
+ request.sourceChannel,
1613
+
1614
+ requesterChatId,
1615
+
1616
+ request.requesterExternalUserId,
1617
+ );
1652
1618
  await deliverChannelReply(
1653
1619
  channelDeliveryContext.replyCallbackUrl,
1654
1620
  grantApprovalPayload,
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { audienceForReader } from "../message-audience.js";
4
+
5
+ describe("audienceForReader", () => {
6
+ test("restricts a reply in a Slack channel to the named reader", () => {
7
+ expect(audienceForReader("slack", "C0ROOM", "U0READER")).toEqual({
8
+ kind: "oneReader",
9
+ userId: "U0READER",
10
+ });
11
+ });
12
+
13
+ test("leaves a Slack DM unrestricted", () => {
14
+ // Nobody else can read a DM, so restricting buys no privacy and costs
15
+ // durability: an ephemeral message does not survive a reload.
16
+ expect(audienceForReader("slack", "D0DIRECT", "U0READER")).toBeUndefined();
17
+ });
18
+
19
+ test("restricts a private group, which several readers share", () => {
20
+ // A `G` id is a shared room like a `C` one. Testing for a DM rather than
21
+ // for a channel id is what keeps this from leaking into the group.
22
+ expect(audienceForReader("slack", "G0PRIVATE", "U0READER")).toEqual({
23
+ kind: "oneReader",
24
+ userId: "U0READER",
25
+ });
26
+ });
27
+
28
+ test("leaves every other channel unrestricted", () => {
29
+ for (const channel of ["telegram", "discord", "whatsapp", "email"]) {
30
+ expect(
31
+ audienceForReader(channel, "C0ROOM", "U0READER"),
32
+ `${channel} has no way to show one reader a message in a shared room`,
33
+ ).toBeUndefined();
34
+ }
35
+ });
36
+
37
+ test("stays unrestricted when there is no reader to name", () => {
38
+ expect(audienceForReader("slack", "C0ROOM", undefined)).toBeUndefined();
39
+ expect(audienceForReader("slack", "C0ROOM", null)).toBeUndefined();
40
+ });
41
+
42
+ test("tolerates an absent channel or room rather than throwing", () => {
43
+ // Several callers hold values that were never narrowed, so the answer for
44
+ // an unknown channel has to be the safe one rather than an exception.
45
+ expect(audienceForReader(undefined, "C0ROOM", "U0READER")).toBeUndefined();
46
+ expect(audienceForReader(null, "C0ROOM", "U0READER")).toBeUndefined();
47
+ expect(audienceForReader("slack", null, "U0READER")).toBeUndefined();
48
+ });
49
+ });
@@ -17,9 +17,10 @@ describe("INTERFACE_IDS", () => {
17
17
  ).toBe(true);
18
18
  });
19
19
 
20
- test("still includes macos and other existing interfaces", () => {
20
+ test("includes desktop and existing interfaces", () => {
21
21
  for (const id of [
22
22
  "macos",
23
+ "windows",
23
24
  "ios",
24
25
  "cli",
25
26
  "telegram",
@@ -41,8 +42,9 @@ describe("INTERACTIVE_INTERFACES", () => {
41
42
  expect(INTERACTIVE_INTERFACES.has("chrome-extension" as never)).toBe(false);
42
43
  });
43
44
 
44
- test("still includes macos", () => {
45
+ test("includes desktop clients", () => {
45
46
  expect(INTERACTIVE_INTERFACES.has("macos")).toBe(true);
47
+ expect(INTERACTIVE_INTERFACES.has("windows")).toBe(true);
46
48
  });
47
49
  });
48
50
 
@@ -51,8 +53,9 @@ describe("isInterfaceId", () => {
51
53
  expect(isInterfaceId("chrome-extension")).toBe(true);
52
54
  });
53
55
 
54
- test("returns true for macos", () => {
56
+ test("returns true for desktop interfaces", () => {
55
57
  expect(isInterfaceId("macos")).toBe(true);
58
+ expect(isInterfaceId("windows")).toBe(true);
56
59
  });
57
60
 
58
61
  test("returns false for unknown interface", () => {
@@ -68,6 +71,7 @@ describe("parseInterfaceId", () => {
68
71
  test("returns canonical ID for valid interface", () => {
69
72
  expect(parseInterfaceId("web")).toBe("web");
70
73
  expect(parseInterfaceId("macos")).toBe("macos");
74
+ expect(parseInterfaceId("windows")).toBe("windows");
71
75
  });
72
76
 
73
77
  test("normalizes legacy 'vellum' alias to 'web'", () => {
@@ -103,6 +107,16 @@ describe("supportsHostProxy", () => {
103
107
  expect(supportsHostProxy("macos", "host_browser")).toBe(true);
104
108
  });
105
109
 
110
+ test("windows exposes its implemented host capabilities", () => {
111
+ expect(supportsHostProxy("windows")).toBe(true);
112
+ expect(supportsHostProxy("windows", "host_bash")).toBe(true);
113
+ expect(supportsHostProxy("windows", "host_file")).toBe(true);
114
+ expect(supportsHostProxy("windows", "host_cu")).toBe(true);
115
+ expect(supportsHostProxy("windows", "host_browser")).toBe(true);
116
+ expect(supportsHostProxy("windows", "host_ui_snapshot")).toBe(true);
117
+ expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
118
+ });
119
+
106
120
  // ── chrome-extension: only host_browser. ──
107
121
  test("chrome-extension returns false (no capability)", () => {
108
122
  // Chrome extension does not support "any host proxy at all" — it only
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Who a reply should be visible to, decided once for every producer.
3
+ */
4
+
5
+ import type { MessageAudience } from "@vellumai/gateway-client";
6
+
7
+ import { isSlackDmConversation } from "../messaging/providers/slack/message-metadata.js";
8
+
9
+ /**
10
+ * The audience a reply addressed at `userId` should carry, or `undefined` when
11
+ * the whole room may see it.
12
+ *
13
+ * Restricting a message only means something where someone else could read it.
14
+ * A room with one reader gets no restriction: a Slack DM is already private,
15
+ * and marking it restricted there costs durability, because an ephemeral
16
+ * message does not survive a reload.
17
+ *
18
+ * Slack is the only channel that can show one reader a message in a room it
19
+ * shares with others. The test is for a DM rather than for a channel id, so
20
+ * every other room shape counts as shared: a private group holds several
21
+ * readers even though its id begins with `G` rather than `C`.
22
+ *
23
+ * `sourceChannel` and `chatId` are loosely typed because the only questions
24
+ * asked of them are whether the channel is Slack and whether the room is a
25
+ * DM, and several callers hold values that were never narrowed. An unknown
26
+ * channel answers with no restriction rather than throwing.
27
+ */
28
+ export function audienceForReader(
29
+ sourceChannel: string | null | undefined,
30
+ chatId: string | null | undefined,
31
+ userId: string | null | undefined,
32
+ ): MessageAudience | undefined {
33
+ if (sourceChannel !== "slack" || !userId || !chatId) {
34
+ return undefined;
35
+ }
36
+ if (isSlackDmConversation(chatId)) {
37
+ return undefined;
38
+ }
39
+ return { kind: "oneReader", userId };
40
+ }
@@ -166,6 +166,7 @@ export const CHANNEL_METADATA: Partial<Record<ChannelId, ChannelInfo>> = {
166
166
 
167
167
  export const INTERFACE_IDS = [
168
168
  "macos",
169
+ "windows",
169
170
  "ios",
170
171
  "cli",
171
172
  "telegram",
@@ -264,6 +265,7 @@ export function parseClientOs(value: unknown): ClientOs | null {
264
265
  */
265
266
  export const INTERACTIVE_INTERFACES: ReadonlySet<InterfaceId> = new Set([
266
267
  "macos",
268
+ "windows",
267
269
  "ios",
268
270
  "cli",
269
271
  "web",
@@ -274,9 +276,9 @@ export function isInteractiveInterface(id: InterfaceId): boolean {
274
276
  }
275
277
 
276
278
  /**
277
- * Host proxy capabilities that an interface can support. The macOS client
278
- * supports all of them; the chrome-extension interface only supports
279
- * host_browser (via the Chrome DevTools Protocol proxy).
279
+ * Host proxy capabilities that an interface can support. macOS supports all
280
+ * of them, Windows withholds app control, and chrome-extension supports only
281
+ * host_browser through the Chrome DevTools Protocol proxy.
280
282
  */
281
283
  export const HOST_PROXY_CAPABILITIES = [
282
284
  "host_bash",
@@ -290,29 +292,28 @@ export const HOST_PROXY_CAPABILITIES = [
290
292
  export type HostProxyCapability = (typeof HOST_PROXY_CAPABILITIES)[number];
291
293
 
292
294
  /**
293
- * Interfaces that support the full desktop host-proxy set (every
294
- * `HostProxyCapability` value). This is the capability-level identity used
295
- * by the discriminated transport metadata union and by the
295
+ * Interfaces that support desktop host-proxy tools. This identity is used by
296
+ * the discriminated transport metadata union and by the
296
297
  * `supportsHostProxy(id)` type predicate.
297
298
  *
298
299
  * Extend this literal type AND the `supportsHostProxy` implementation
299
- * below in lock-step when adding a new host-capable client (e.g. a native
300
- * Linux or Windows desktop).
300
+ * below in lock-step when adding a new host-capable client such as native
301
+ * Linux.
301
302
  */
302
- export type HostProxyInterfaceId = "macos";
303
+ export type HostProxyInterfaceId = "macos" | "windows";
303
304
 
304
305
  /**
305
306
  * Whether the interface supports a host proxy capability.
306
307
  *
307
308
  * The no-arg form `supportsHostProxy(id)` asks "is this interface a desktop
308
- * host-proxy client?" — it returns `true` only for macOS and is the type
309
+ * host-proxy client?" It returns `true` for native desktop clients and is the
309
310
  * predicate that narrows `InterfaceId` to `HostProxyInterfaceId`. It returns
310
311
  * `false` for chrome-extension because chrome-extension only supports
311
312
  * `host_browser`, and the no-arg form is the gate that legacy desktop-only
312
313
  * call sites use (e.g. preactivating computer-use, restoring host proxies
313
- * in the drain queue). Callers that want to check a single capability —
314
+ * in the drain queue). Callers that want to check a single capability,
314
315
  * for example, to decide whether to keep `hostBrowserProxy` available for
315
- * chrome-extension — should pass the capability explicitly:
316
+ * chrome-extension, should pass the capability explicitly:
316
317
  * `supportsHostProxy(id, "host_browser")`.
317
318
  */
318
319
  export function supportsHostProxy(id: InterfaceId): id is HostProxyInterfaceId;
@@ -331,6 +332,9 @@ export function supportsHostProxy(
331
332
  if (id === "macos") {
332
333
  return true;
333
334
  }
335
+ if (id === "windows") {
336
+ return capability == null || capability !== "host_app_control";
337
+ }
334
338
  if (id === "chrome-extension" && capability === "host_browser") {
335
339
  return true;
336
340
  }