@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,133 @@
1
+ /**
2
+ * Where a guardian's own gated-tool prompt is delivered.
3
+ *
4
+ * The prompt is raised by a turn the guardian is having, and that turn can be
5
+ * running in a room: a shared Slack channel, a Telegram group. The card
6
+ * carries the tool name and a preview of the command, so delivering it where
7
+ * the turn is shows both to everyone there. It goes to the guardian's own
8
+ * bound chat instead.
9
+ *
10
+ * The address is only half of it. A callback URL addresses the turn it came
11
+ * from, and a param left on one either fails the send or wins over the new
12
+ * address outright, which is indistinguishable from never having redirected.
13
+ * The assertions below are written against params the transports read today
14
+ * *and* against one nothing reads, because the guarantee is that everything is
15
+ * dropped rather than that a known list is.
16
+ */
17
+
18
+ import { describe, expect, test } from "bun:test";
19
+
20
+ import {
21
+ resolveGuardianPromptDelivery,
22
+ stripTurnDestination,
23
+ } from "../approvals/guardian-channel-delivery.js";
24
+
25
+ import "./test-preload.js";
26
+
27
+ const ROOM = "C0SHARED123";
28
+ const GUARDIAN_CHAT = "D0GUARDIANDM";
29
+
30
+ describe("a prompt raised where the guardian is not alone", () => {
31
+ test("is addressed to the guardian's own chat", () => {
32
+ expect(
33
+ resolveGuardianPromptDelivery({
34
+ turnChatId: ROOM,
35
+ turnCallbackUrl: "https://gw.test/deliver/slack",
36
+ guardianChatId: GUARDIAN_CHAT,
37
+ }).chatId,
38
+ ).toBe(GUARDIAN_CHAT);
39
+ });
40
+
41
+ test("applies to any channel, including ones not built yet", () => {
42
+ // A Telegram group carries the same exposure as a shared Slack channel,
43
+ // and so will whatever is added next. The rule names no channel, so there
44
+ // is no per-channel list to keep in step with the transports.
45
+ for (const [turnChatId, guardianChatId] of [
46
+ ["-1001234567890", "555001"],
47
+ ["guild-channel-1", "dm-channel-1"],
48
+ ["room@some-future-channel", "direct@some-future-channel"],
49
+ ]) {
50
+ expect(
51
+ resolveGuardianPromptDelivery({
52
+ turnChatId,
53
+ turnCallbackUrl: "https://gw.test/deliver/whatever",
54
+ guardianChatId,
55
+ }).chatId,
56
+ ).toBe(guardianChatId);
57
+ }
58
+ });
59
+ });
60
+
61
+ describe("a prompt raised in the guardian's own chat", () => {
62
+ const turnCallbackUrl = "https://gw.test/deliver/slack?threadTs=111.222";
63
+
64
+ test("does not move, and keeps the turn's callback intact", () => {
65
+ expect(
66
+ resolveGuardianPromptDelivery({
67
+ turnChatId: GUARDIAN_CHAT,
68
+ turnCallbackUrl,
69
+ guardianChatId: GUARDIAN_CHAT,
70
+ }),
71
+ ).toEqual({ chatId: GUARDIAN_CHAT, callbackUrl: turnCallbackUrl });
72
+ });
73
+
74
+ test("does not move when no binding resolved", () => {
75
+ // The trust context falls back to the turn's own chat, so this is the same
76
+ // comparison as above: delivery is left exactly as it is today rather than
77
+ // dropped on the floor.
78
+ expect(
79
+ resolveGuardianPromptDelivery({
80
+ turnChatId: ROOM,
81
+ turnCallbackUrl,
82
+ guardianChatId: undefined,
83
+ }),
84
+ ).toEqual({ chatId: ROOM, callbackUrl: turnCallbackUrl });
85
+ });
86
+ });
87
+
88
+ describe("a redirected delivery leaves the turn behind entirely", () => {
89
+ const redirected = (turnCallbackUrl: string) =>
90
+ resolveGuardianPromptDelivery({
91
+ turnChatId: ROOM,
92
+ turnCallbackUrl,
93
+ guardianChatId: GUARDIAN_CHAT,
94
+ }).callbackUrl;
95
+
96
+ test("drops params the transports read today", () => {
97
+ // threadTs: Slack's thread, absent from the new chat.
98
+ // threadId: a Telegram forum topic, and on Discord the destination itself
99
+ // (`sendTarget` returns it in place of the chatId).
100
+ // dm: changes how Discord reads the address.
101
+ // channel: the turn's own chat, which the gateway hangs on every Slack
102
+ // callback.
103
+ expect(
104
+ redirected(
105
+ "https://gw.test/deliver/slack?channel=C1&threadTs=1.2&threadId=9&dm=1&messageTs=3.4",
106
+ ),
107
+ ).toBe("https://gw.test/deliver/slack");
108
+ });
109
+
110
+ test("drops a param nothing reads, which is the actual guarantee", () => {
111
+ // If this only dropped a known list, a transport that starts reading a new
112
+ // param would silently undo the redirect and no test would notice. The
113
+ // contract is that the callback keeps its channel and nothing else.
114
+ expect(redirected("https://gw.test/deliver/x?roomId=42")).toBe(
115
+ "https://gw.test/deliver/x",
116
+ );
117
+ });
118
+
119
+ test("keeps the channel, which is how the transport is resolved", () => {
120
+ // `isDirectDelivery` picks a transport off the path, so the path has to
121
+ // survive for the redirect to be deliverable at all.
122
+ expect(redirected("https://gw.test/deliver/telegram?threadId=7")).toContain(
123
+ "/deliver/telegram",
124
+ );
125
+ });
126
+ });
127
+
128
+ describe("stripTurnDestination", () => {
129
+ test("returns relative or malformed urls untouched", () => {
130
+ // They carry no params, and `new URL` throws on them.
131
+ expect(stripTurnDestination("/deliver/slack")).toBe("/deliver/slack");
132
+ });
133
+ });
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Regression tests for the guardian-verify-setup skill.
3
3
  *
4
- * Ensures the voice verification flow includes proactive auto-check polling
5
- * so the user does not have to manually ask whether verification succeeded.
4
+ * The behaviour under guard is proactive auto-check polling: after a code is
5
+ * delivered, the skill must poll for completion rather than leaving the user
6
+ * to ask whether it worked. Originally that existed only for voice, and these
7
+ * tests pinned the voice section. The three per-channel polling sections were
8
+ * later merged into one parameterised section, so the assertions here pin the
9
+ * behaviour in its shared form and cover every channel that polls.
6
10
  */
7
11
 
8
12
  import { readFileSync } from "node:fs";
@@ -23,128 +27,170 @@ const SKILL_PATH = resolve(
23
27
 
24
28
  const skillContent = readFileSync(SKILL_PATH, "utf-8");
25
29
 
30
+ /**
31
+ * The channels that poll. Telegram is deliberately absent: it confirms through
32
+ * its own bot-driven flow, so polling it reports nothing.
33
+ */
34
+ const POLLED_CHANNELS = ["phone", "slack", "discord", "email"] as const;
35
+
36
+ function section(from: string, to: string): string {
37
+ const body = skillContent.split(from)[1]?.split(to)[0];
38
+ // An empty slice would make every `toContain` below fail loudly rather than
39
+ // silently pass, which is the intent: a renamed heading must break this.
40
+ return body ?? "";
41
+ }
42
+
43
+ const pollingSection = () => section("## Auto-Check Polling", "## Step 6");
44
+
26
45
  // ---------------------------------------------------------------------------
27
46
  // Tests
28
47
  // ---------------------------------------------------------------------------
29
48
 
30
- describe("guardian-verify-setup skill — voice auto-followup", () => {
31
- test("voice path in Step 3 references the auto-check polling loop", () => {
32
- // The voice success instruction in Step 3 must direct the assistant to
33
- // begin the polling loop rather than waiting for the user to report back.
34
- expect(skillContent).toContain(
35
- "immediately begin the voice auto-check polling loop",
36
- );
49
+ describe("guardian-verify-setup skill: proactive auto-check polling", () => {
50
+ test("the polling section exists", () => {
51
+ expect(skillContent).toContain("## Auto-Check Polling");
52
+ });
53
+
54
+ test("Step 3 sends every polled channel into the loop", () => {
55
+ const step3 = section("## Step 3", "## Step 4");
56
+ for (const channel of POLLED_CHANNELS) {
57
+ const bullet = step3
58
+ .split("\n")
59
+ .filter((line) =>
60
+ new RegExp(
61
+ `^\\s*-\\s+\\*\\*(Phone|Slack|Discord|Email)\\*\\*`,
62
+ "i",
63
+ ).test(line),
64
+ )
65
+ .find((line) => new RegExp(channel, "i").test(line));
66
+ expect(bullet, `Step 3 has no bullet for ${channel}`).toBeDefined();
67
+ expect(bullet).toContain("auto-check polling loop");
68
+ }
37
69
  });
38
70
 
39
- test("voice path in Step 4 (resend) references the auto-check polling loop", () => {
40
- // After a voice resend, the same auto-check behavior must kick in.
41
- const resendSection =
42
- skillContent.split("## Step 4")[1]?.split("## Step 5")[0] ?? "";
43
- expect(resendSection).toContain("voice auto-check polling loop");
71
+ test("Step 4 resend sends every polled channel back into the loop", () => {
72
+ const resend = section("## Step 4", "## Step 5");
73
+ for (const channel of POLLED_CHANNELS) {
74
+ expect(
75
+ new RegExp(`\\*\\*${channel}\\*\\*`, "i").test(resend),
76
+ `Step 4 has no bullet for ${channel}`,
77
+ ).toBe(true);
78
+ }
79
+ expect(resend).toContain("auto-check polling loop");
44
80
  });
45
81
 
46
- test("contains a Voice Auto-Check Polling section", () => {
47
- expect(skillContent).toContain("## Voice Auto-Check Polling");
82
+ test("the polling section names every polled channel and excludes Telegram", () => {
83
+ const body = pollingSection();
84
+ for (const channel of POLLED_CHANNELS) {
85
+ expect(body, `polling section omits ${channel}`).toContain(channel);
86
+ }
87
+ // Telegram may only appear as an exclusion, never as a channel to poll.
88
+ expect(body).toContain("Never Telegram");
48
89
  });
49
90
 
50
- test("polling section specifies the correct status command for voice", () => {
51
- const pollingSection =
52
- skillContent
53
- .split("## Voice Auto-Check Polling")[1]
54
- ?.split("## Step 6")[0] ?? "";
55
- expect(pollingSection).toContain(
56
- "assistant channel-verification-sessions status --channel phone --json",
91
+ test("the polling command is guarded against an unset channel", () => {
92
+ const body = pollingSection();
93
+ // The command is parameterised, so the channel has to be assigned in the
94
+ // same block. An unset CHANNEL would call the CLI with an empty value.
95
+ expect(body).toContain('CHANNEL=""');
96
+ expect(body).toContain('if [ -z "$CHANNEL" ]');
97
+ expect(body).toContain(
98
+ 'channel-verification-sessions status --channel "$CHANNEL" --json',
57
99
  );
58
100
  });
59
101
 
60
- test("polling section includes ~15 second interval", () => {
61
- const pollingSection =
62
- skillContent
63
- .split("## Voice Auto-Check Polling")[1]
64
- ?.split("## Step 6")[0] ?? "";
65
- expect(pollingSection).toContain("~15 seconds");
102
+ test("the polling section states an interval and a timeout per channel", () => {
103
+ const body = pollingSection();
104
+ expect(body).toContain("15s");
105
+ expect(body).toContain("20s");
106
+ expect(body).toContain("2 minutes");
107
+ expect(body).toContain("3 minutes");
66
108
  });
67
109
 
68
- test("polling section includes 2-minute timeout", () => {
69
- const pollingSection =
70
- skillContent
71
- .split("## Voice Auto-Check Polling")[1]
72
- ?.split("## Step 6")[0] ?? "";
73
- expect(pollingSection).toContain("2 minutes");
110
+ test("the polling section checks for bound: true", () => {
111
+ expect(pollingSection()).toContain("bound: true");
74
112
  });
75
113
 
76
- test("polling section checks for bound: true", () => {
77
- const pollingSection =
78
- skillContent
79
- .split("## Voice Auto-Check Polling")[1]
80
- ?.split("## Step 6")[0] ?? "";
81
- expect(pollingSection).toContain("bound: true");
114
+ test("success is reported proactively, not on request", () => {
115
+ const body = pollingSection();
116
+ expect(body).toContain("success message");
117
+ expect(body).toContain("Do NOT require the user to ask");
82
118
  });
83
119
 
84
- test("polling section includes proactive success confirmation", () => {
85
- const pollingSection =
86
- skillContent
87
- .split("## Voice Auto-Check Polling")[1]
88
- ?.split("## Step 6")[0] ?? "";
89
- expect(pollingSection).toContain("proactive success message");
120
+ test("timeout offers a resend rather than stopping silently", () => {
121
+ const body = pollingSection();
122
+ expect(body).toContain("timeout");
123
+ expect(body).toContain("resend");
90
124
  });
91
125
 
92
- test("polling section includes timeout fallback with resend/restart offer", () => {
93
- const pollingSection =
94
- skillContent
95
- .split("## Voice Auto-Check Polling")[1]
96
- ?.split("## Step 6")[0] ?? "";
97
- expect(pollingSection).toContain("timeout");
98
- expect(pollingSection).toContain("resend");
126
+ test("the rebind guard survives, with its false-success reasoning", () => {
127
+ const body = pollingSection();
128
+ expect(body).toContain("Rebind guard");
129
+ expect(body).toContain("verificationSessionId");
130
+ expect(body).toContain("Non-rebind flows");
99
131
  });
100
132
 
101
- test("polling section includes rebind guard against false-success from pre-existing binding", () => {
102
- const pollingSection =
103
- skillContent
104
- .split("## Voice Auto-Check Polling")[1]
105
- ?.split("## Step 6")[0] ?? "";
106
- // Must mention rebind guard concept
107
- expect(pollingSection).toContain("Rebind guard");
108
- // Must instruct not to trust bound: true alone in a rebind flow
109
- expect(pollingSection).toContain(
110
- "do NOT treat `bound: true` alone as success",
111
- );
112
- // Must reference verificationSessionId as the mechanism to detect fresh binding
113
- expect(pollingSection).toContain("verificationSessionId");
114
- // Must clarify non-rebind flows are unaffected
115
- expect(pollingSection).toContain("Non-rebind flows");
133
+ test("no polled channel's Step 3 bullet tells the assistant to wait", () => {
134
+ const step3 = section("## Step 3", "## Step 4");
135
+ // Narrowed to the polled bullets: Telegram's "wait for the user to confirm
136
+ // they clicked the link" is a real instruction for its own bootstrap flow.
137
+ const bullets = step3
138
+ .split("\n")
139
+ .filter((line) =>
140
+ /^\s*-\s+\*\*(Phone|Slack|Discord|Email)\*\*/i.test(line),
141
+ );
142
+ expect(bullets.length).toBe(POLLED_CHANNELS.length);
143
+ for (const bullet of bullets) {
144
+ expect(bullet).not.toContain("wait for the user to confirm");
145
+ expect(bullet).not.toContain("ask the user if it worked");
146
+ }
147
+ });
148
+ });
149
+
150
+ describe("guardian-verify-setup skill: channel coverage", () => {
151
+ test("every channel the CLI accepts is offered in Step 1", () => {
152
+ const step1 = section("## Step 1", "## Step 2");
153
+ for (const channel of [...POLLED_CHANNELS, "telegram"]) {
154
+ expect(step1, `Step 1 does not offer ${channel}`).toContain(
155
+ `**${channel}**`,
156
+ );
157
+ }
158
+ });
159
+
160
+ test("Step 2 collects a destination for every channel", () => {
161
+ const step2 = section("## Step 2", "## Step 3");
162
+ for (const channel of ["Phone", "Telegram", "Slack", "Discord", "Email"]) {
163
+ expect(step2, `Step 2 has no destination for ${channel}`).toContain(
164
+ `**${channel}**`,
165
+ );
166
+ }
116
167
  });
117
168
 
118
- test("polling is voice-only — does not apply to Telegram", () => {
119
- const pollingSection =
120
- skillContent
121
- .split("## Voice Auto-Check Polling")[1]
122
- ?.split("## Step 6")[0] ?? "";
123
- expect(pollingSection).toContain("voice-only");
124
- expect(pollingSection).toContain("Do NOT poll for Telegram");
169
+ test("the missing-secret guardrail names only its exception", () => {
170
+ const guardrail = skillContent
171
+ .split("\n")
172
+ .find((line) => line.includes("Missing `secret` guardrail"));
173
+ expect(guardrail).toBeDefined();
174
+ // The rule covers every flow but one, so the line states that exception.
175
+ // Naming any other channel makes it an enumeration, and an enumeration
176
+ // drifts out of date the next time a channel is added.
177
+ expect(guardrail).toContain("except");
178
+ expect(guardrail).toContain("Telegram");
179
+ const lower = guardrail!.toLowerCase();
180
+ for (const channel of ["phone", "voice", "slack", "discord", "email"]) {
181
+ expect(lower, `guardrail enumerates ${channel}`).not.toContain(channel);
182
+ }
125
183
  });
126
184
 
127
- test('no instruction requires waiting for user to ask "did it work?"', () => {
128
- // The skill should never instruct the assistant to wait for the user to
129
- // confirm that voice verification worked. The auto-check polling loop
130
- // makes this unnecessary.
131
- const voiceAutoCheckSection =
132
- skillContent
133
- .split("## Voice Auto-Check Polling")[1]
134
- ?.split("## Step 6")[0] ?? "";
135
- expect(voiceAutoCheckSection).toContain("Do NOT require the user to ask");
136
- // The voice bullet in Step 3 should not instruct the assistant to wait
137
- // for the user to confirm or ask if it worked. Narrow to just the voice
138
- // bullet line to avoid false positives from Telegram's "wait for the
139
- // user to confirm they clicked the link" which is unrelated to voice.
140
- const step3Section =
141
- skillContent.split("## Step 3")[1]?.split("## Step 4")[0] ?? "";
142
- const voiceBullet = step3Section
185
+ test("the status and revoke guards allow every supported channel", () => {
186
+ // These enumerate valid CHANNEL values. A channel missing here reads to
187
+ // the assistant as unsupported at exactly the point a user asks for it.
188
+ const guards = skillContent
143
189
  .split("\n")
144
- .filter((line) => /^\s*-\s+\*\*Phone\*\*/.test(line))
145
- .join("\n");
146
- expect(voiceBullet).not.toHaveLength(0);
147
- expect(voiceBullet).not.toContain("wait for the user to confirm");
148
- expect(voiceBullet).not.toContain("ask the user if it worked");
190
+ .filter((line) => line.includes("MUST set to one of"));
191
+ expect(guards.length).toBeGreaterThanOrEqual(2);
192
+ for (const guard of guards) {
193
+ expect(guard).toContain("discord");
194
+ }
149
195
  });
150
196
  });
@@ -17,7 +17,7 @@
17
17
  * installIpcMock();
18
18
  *
19
19
  * mockIpcResponse("classify_risk",
20
- * { risk: "low", reason: "ls", matchType: "shell" });
20
+ * { risk: "low", reason: "ls", matchType: "registry", scopeOptions: [] });
21
21
  *
22
22
  * const result = await classifyRisk("bash", { command: "ls" });
23
23
  */
@@ -36,12 +36,26 @@ export function mockIpcResponse(method: string, response: unknown): void {
36
36
  responses.set(method, response);
37
37
  }
38
38
 
39
- /** Clear all registered responses. */
39
+ /** Clear all registered responses and captured params. */
40
40
  export function clearIpcMocks(): void {
41
41
  responses.clear();
42
+ lastParams.clear();
42
43
  }
43
44
 
44
- function handleCall(method: string): unknown {
45
+ const lastParams = new Map<string, Record<string, unknown> | undefined>();
46
+
47
+ /**
48
+ * The params of the most recent call to `method`, for asserting what the
49
+ * daemon sent rather than what it did with the answer.
50
+ */
51
+ export function lastIpcParams(
52
+ method: string,
53
+ ): Record<string, unknown> | undefined {
54
+ return lastParams.get(method);
55
+ }
56
+
57
+ function handleCall(method: string, params?: Record<string, unknown>): unknown {
58
+ lastParams.set(method, params);
45
59
  const response = responses.get(method);
46
60
  if (response !== undefined) {
47
61
  return response;
@@ -57,11 +71,18 @@ function handleCall(method: string): unknown {
57
71
  */
58
72
  export function installIpcMock(): void {
59
73
  mock.module("@vellumai/gateway-client/ipc-client", () => ({
60
- ipcCall: async (_socketPath: string, method: string) => handleCall(method),
74
+ ipcCall: async (
75
+ _socketPath: string,
76
+ method: string,
77
+ params?: Record<string, unknown>,
78
+ ) => handleCall(method, params),
61
79
 
62
80
  PersistentIpcClient: class MockPersistentIpcClient {
63
- async call(method: string): Promise<unknown> {
64
- return handleCall(method);
81
+ async call(
82
+ method: string,
83
+ params?: Record<string, unknown>,
84
+ ): Promise<unknown> {
85
+ return handleCall(method, params);
65
86
  }
66
87
  destroy(): void {}
67
88
  },
@@ -16,6 +16,7 @@ import { isHostProxyTransport } from "../daemon/message-types/conversations.js";
16
16
  describe("supportsHostProxy (runtime)", () => {
17
17
  test("no-arg form returns true for host-proxy interfaces", () => {
18
18
  expect(supportsHostProxy("macos")).toBe(true);
19
+ expect(supportsHostProxy("windows")).toBe(true);
19
20
  });
20
21
 
21
22
  test("no-arg form returns false for interfaces without host-proxy support", () => {
@@ -42,13 +43,22 @@ describe("supportsHostProxy (runtime)", () => {
42
43
  expect(supportsHostProxy("chrome-extension", "host_cu")).toBe(false);
43
44
  });
44
45
 
45
- test("capability form grants all four capabilities to macOS including host_browser", () => {
46
+ test("capability form grants representative host capabilities to macOS", () => {
46
47
  expect(supportsHostProxy("macos", "host_bash")).toBe(true);
47
48
  expect(supportsHostProxy("macos", "host_file")).toBe(true);
48
49
  expect(supportsHostProxy("macos", "host_cu")).toBe(true);
49
50
  expect(supportsHostProxy("macos", "host_browser")).toBe(true);
50
51
  });
51
52
 
53
+ test("capability form withholds unimplemented Windows app control", () => {
54
+ expect(supportsHostProxy("windows", "host_bash")).toBe(true);
55
+ expect(supportsHostProxy("windows", "host_file")).toBe(true);
56
+ expect(supportsHostProxy("windows", "host_cu")).toBe(true);
57
+ expect(supportsHostProxy("windows", "host_browser")).toBe(true);
58
+ expect(supportsHostProxy("windows", "host_ui_snapshot")).toBe(true);
59
+ expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
60
+ });
61
+
52
62
  test("capability form rejects everything for non-host-proxy interfaces", () => {
53
63
  expect(supportsHostProxy("ios", "host_bash")).toBe(false);
54
64
  expect(supportsHostProxy("cli", "host_file")).toBe(false);
@@ -678,6 +678,14 @@ describe("host_bash — spawn error handling", () => {
678
678
  // ---------------------------------------------------------------------------
679
679
 
680
680
  describe("host_bash — proxy delegation", () => {
681
+ // Under tmpdir so the live-workspace guard accepts it as a workspace.
682
+ const FAKE_INSTANCE_WS = join(
683
+ tmpdir(),
684
+ "vellum-instance",
685
+ ".vellum",
686
+ "workspace",
687
+ );
688
+
681
689
  const ROUTING_ENV_KEYS = [
682
690
  "VELLUM_WORKSPACE_DIR",
683
691
  "VELLUM_DATA_DIR",
@@ -850,6 +858,11 @@ describe("host_bash — proxy delegation", () => {
850
858
  for (const key of ROUTING_ENV_KEYS) {
851
859
  delete process.env[key];
852
860
  }
861
+ // With VELLUM_WORKSPACE_DIR unset, loadConfig resolves the real ~/.vellum
862
+ // fallback (read-only here), so bypass the live-workspace guard for it.
863
+ const originalAllowRealWorkspace =
864
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
865
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
853
866
 
854
867
  try {
855
868
  const proxyResult: ToolExecutionResult = {
@@ -870,14 +883,20 @@ describe("host_bash — proxy delegation", () => {
870
883
  __REVEAL_NONCE: expect.any(String),
871
884
  });
872
885
  } finally {
886
+ if (originalAllowRealWorkspace === undefined) {
887
+ delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
888
+ } else {
889
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS =
890
+ originalAllowRealWorkspace;
891
+ }
873
892
  restoreEnv(envSnapshot);
874
893
  }
875
894
  });
876
895
 
877
896
  test("propagates daemon routing env vars to proxy for nested assistant CLI calls", async () => {
878
897
  const envSnapshot = captureEnv(ROUTING_ENV_KEYS);
879
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-instance/.vellum/workspace";
880
- process.env.VELLUM_DATA_DIR = "/tmp/vellum-instance/.vellum/workspace/data";
898
+ process.env.VELLUM_WORKSPACE_DIR = FAKE_INSTANCE_WS;
899
+ process.env.VELLUM_DATA_DIR = join(FAKE_INSTANCE_WS, "data");
881
900
  process.env.VELLUM_ENVIRONMENT = "local";
882
901
  process.env.INTERNAL_GATEWAY_BASE_URL = "http://127.0.0.1:7830";
883
902
 
@@ -896,8 +915,8 @@ describe("host_bash — proxy delegation", () => {
896
915
  expect(result).toBe(proxyResult);
897
916
  expect(calls.length).toBe(1);
898
917
  expect(calls[0].input.env).toEqual({
899
- VELLUM_WORKSPACE_DIR: "/tmp/vellum-instance/.vellum/workspace",
900
- VELLUM_DATA_DIR: "/tmp/vellum-instance/.vellum/workspace/data",
918
+ VELLUM_WORKSPACE_DIR: FAKE_INSTANCE_WS,
919
+ VELLUM_DATA_DIR: join(FAKE_INSTANCE_WS, "data"),
901
920
  VELLUM_ENVIRONMENT: "local",
902
921
  INTERNAL_GATEWAY_BASE_URL: "http://127.0.0.1:7830",
903
922
  __CONVERSATION_ID: "test-conversation",