@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,50 @@
1
+ /**
2
+ * Memory injection indexes CLI help via {@link buildCliCommandHelpContent}.
3
+ * Setup prompts like "Setup <app> for me" should retrieve `plugins search` /
4
+ * `skills search` before the model falls back to web search.
5
+ *
6
+ * These assertions lock the retrieval phrases in the indexed blob so a later
7
+ * wording change cannot silently drop them.
8
+ */
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import { buildCliCommandHelpContent } from "../../plugins/defaults/memory/substrate/cli-command-content.js";
12
+ import { pluginsHelp } from "../commands/plugins.help.js";
13
+ import { skillsHelp } from "../commands/skills.help.js";
14
+
15
+ function searchHelp(
16
+ help: { subcommands?: Array<{ name: string; helpText?: string }> },
17
+ ): { name: string; helpText?: string } {
18
+ const search = help.subcommands?.find((sub) => sub.name === "search");
19
+ if (!search) {
20
+ throw new Error("expected a search subcommand");
21
+ }
22
+ return search;
23
+ }
24
+
25
+ describe("catalog search help for setup-intent retrieval", () => {
26
+ test("plugins help indexes setup, superpowers, and web-search fallback", () => {
27
+ const indexed = buildCliCommandHelpContent(pluginsHelp);
28
+ const search = searchHelp(pluginsHelp);
29
+
30
+ expect(search.helpText).toBeDefined();
31
+ expect(indexed.toLowerCase()).toContain("superpowers");
32
+ expect(indexed).toContain("Setup <app> for me");
33
+ expect(indexed.toLowerCase()).toContain("before searching the web");
34
+ expect(indexed).toContain("assistant plugins search");
35
+ expect(indexed).toContain("assistant skills search");
36
+ expect(indexed.toLowerCase()).not.toContain("empty query");
37
+ });
38
+
39
+ test("skills help indexes setup as the second stop before web search", () => {
40
+ const indexed = buildCliCommandHelpContent(skillsHelp);
41
+ const search = searchHelp(skillsHelp);
42
+
43
+ expect(search.helpText).toBeDefined();
44
+ expect(indexed).toContain("Second stop after plugin search");
45
+ expect(indexed).toContain("Setup <app> for me");
46
+ expect(indexed.toLowerCase()).toContain("before searching the web");
47
+ expect(indexed).toContain("assistant skills search");
48
+ expect(indexed).toContain("try web search");
49
+ });
50
+ });
@@ -13,10 +13,12 @@
13
13
  * - `revoke` with no flags (channel: undefined)
14
14
  * - `revoke --channel phone` sends channel param
15
15
  * - `create --channel fax` → invalid channel, exits with code 1, IPC not called
16
+ * - the accepted `--channel` set is the canonical CHANNEL_IDS vocabulary
16
17
  */
17
18
 
18
19
  import { beforeEach, describe, expect, mock, test } from "bun:test";
19
20
 
21
+ import { CHANNEL_IDS } from "@vellumai/service-contracts/channels";
20
22
  import { Command } from "commander";
21
23
 
22
24
  // ---------------------------------------------------------------------------
@@ -242,6 +244,35 @@ describe("channel-verification-sessions create", () => {
242
244
  expect(lastIpcCall).toBeNull();
243
245
  });
244
246
 
247
+ test("--channel discord reaches IPC (canonical vocabulary, not a local copy)", async () => {
248
+ const { exitCode } = await runCommand([
249
+ "channel-verification-sessions",
250
+ "create",
251
+ "--channel",
252
+ "discord",
253
+ ]);
254
+
255
+ expect(exitCode).toBe(0);
256
+ expect(lastIpcCall!.method).toBe("channel_verification_sessions_create");
257
+ expect(lastIpcCall!.params).toMatchObject({
258
+ body: { channel: "discord", purpose: "guardian" },
259
+ });
260
+ });
261
+
262
+ test("every canonical channel id is accepted", async () => {
263
+ for (const channel of CHANNEL_IDS) {
264
+ const { exitCode } = await runCommand([
265
+ "channel-verification-sessions",
266
+ "create",
267
+ "--channel",
268
+ channel,
269
+ ]);
270
+
271
+ expect(exitCode).toBe(0);
272
+ expect(lastIpcCall!.params).toMatchObject({ body: { channel } });
273
+ }
274
+ });
275
+
245
276
  test("IPC error results in exit code 1", async () => {
246
277
  mockIpcResult = { ok: false, error: "Could not connect" };
247
278
 
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Tests for `assistant conversations search`.
3
+ *
4
+ * Validates IPC plumbing onto `conversations_search`, human vs `--json`
5
+ * output, empty-result copy, and validation of the search term / --limit.
6
+ */
7
+
8
+ import * as nodeFs from "node:fs";
9
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ import { Command } from "commander";
12
+
13
+ let lastIpcCall: { method: string; params?: Record<string, unknown> } | null =
14
+ null;
15
+
16
+ let mockIpcResult: {
17
+ ok: boolean;
18
+ result?: unknown;
19
+ error?: string;
20
+ statusCode?: number;
21
+ } = { ok: true, result: { query: "flux", results: [] } };
22
+
23
+ mock.module("../../../ipc/cli-client.js", () => ({
24
+ cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
25
+ lastIpcCall = { method, params };
26
+ return mockIpcResult;
27
+ },
28
+ exitCodeFromIpcResult: (r: { statusCode?: number }) =>
29
+ r.statusCode === undefined
30
+ ? 10
31
+ : r.statusCode >= 500
32
+ ? 3
33
+ : r.statusCode >= 400
34
+ ? 2
35
+ : 1,
36
+ exitFromIpcResult: () => {
37
+ process.exitCode = 1;
38
+ },
39
+ }));
40
+
41
+ const fakeLogger = {
42
+ info: () => {},
43
+ warn: () => {},
44
+ error: () => {},
45
+ debug: () => {},
46
+ };
47
+
48
+ mock.module("../../../util/logger.js", () => ({
49
+ getLogger: () => fakeLogger,
50
+ getCliLogger: () => fakeLogger,
51
+ initLogger: () => {},
52
+ truncateForLog: (v: string) => v,
53
+ pruneOldLogFiles: () => 0,
54
+ LOG_FILE_PATTERN: /^assistant-(\d{4}-\d{2}-\d{2})\.log$/,
55
+ getCurrentLogFilePath: () => "/tmp/test-assistant.log",
56
+ }));
57
+
58
+ const realFs = { ...nodeFs };
59
+ mock.module("node:fs", () => ({ ...realFs }));
60
+
61
+ const { registerConversationsCommand } = await import("../conversations.js");
62
+
63
+ const THREE_HOURS_AGO = Date.now() - 3 * 60 * 60 * 1000;
64
+
65
+ const matchingResult = {
66
+ conversationId: "conv-xyz",
67
+ conversationTitle: "Quarterly metrics",
68
+ conversationUpdatedAt: THREE_HOURS_AGO,
69
+ matchingMessages: [
70
+ {
71
+ messageId: "msg-1",
72
+ role: "user",
73
+ excerpt: "let's review the flux capacitor numbers",
74
+ createdAt: THREE_HOURS_AGO,
75
+ },
76
+ {
77
+ messageId: "msg-2",
78
+ role: "assistant",
79
+ excerpt: "the Q3 flux capacitor spend is on track",
80
+ createdAt: THREE_HOURS_AGO,
81
+ },
82
+ ],
83
+ };
84
+
85
+ async function runSearch(
86
+ args: string[],
87
+ ): Promise<{ code: number; stdout: string; stderr: string }> {
88
+ const stdoutChunks: string[] = [];
89
+ const stderrChunks: string[] = [];
90
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
91
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
92
+ process.stdout.write = ((chunk: unknown) => {
93
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
94
+ return true;
95
+ }) as typeof process.stdout.write;
96
+ process.stderr.write = ((chunk: unknown) => {
97
+ stderrChunks.push(typeof chunk === "string" ? chunk : String(chunk));
98
+ return true;
99
+ }) as typeof process.stderr.write;
100
+
101
+ const program = new Command();
102
+ program.exitOverride();
103
+ program.configureOutput({
104
+ writeErr: (s) => {
105
+ stderrChunks.push(s);
106
+ },
107
+ writeOut: (s) => {
108
+ stdoutChunks.push(s);
109
+ },
110
+ });
111
+ registerConversationsCommand(program);
112
+ try {
113
+ await program.parseAsync(["node", "assistant", "conversations", ...args]);
114
+ } catch (err) {
115
+ const isHelp =
116
+ typeof err === "object" &&
117
+ err !== null &&
118
+ "code" in err &&
119
+ (err as { code?: string }).code === "commander.helpDisplayed";
120
+ if (!isHelp && (process.exitCode === 0 || process.exitCode === undefined)) {
121
+ process.exitCode = 1;
122
+ }
123
+ if (
124
+ !isHelp &&
125
+ err instanceof Error &&
126
+ !stderrChunks.some((s) => s.includes(err.message))
127
+ ) {
128
+ stderrChunks.push(err.message);
129
+ }
130
+ } finally {
131
+ process.stdout.write = originalStdoutWrite;
132
+ process.stderr.write = originalStderrWrite;
133
+ }
134
+ const code = Number(process.exitCode ?? 0);
135
+ process.exitCode = 0;
136
+ return {
137
+ code,
138
+ stdout: stdoutChunks.join(""),
139
+ stderr: stderrChunks.join(""),
140
+ };
141
+ }
142
+
143
+ beforeEach(() => {
144
+ lastIpcCall = null;
145
+ process.exitCode = 0;
146
+ mockIpcResult = { ok: true, result: { query: "flux", results: [] } };
147
+ });
148
+
149
+ afterEach(() => {
150
+ process.exitCode = 0;
151
+ });
152
+
153
+ describe("conversations search", () => {
154
+ test("forwards the term to conversations_search as queryParams.q", async () => {
155
+ mockIpcResult = {
156
+ ok: true,
157
+ result: { query: "flux capacitor", results: [matchingResult] },
158
+ };
159
+ const { code } = await runSearch(["search", "flux capacitor"]);
160
+ expect(code).toBe(0);
161
+ expect(lastIpcCall?.method).toBe("conversations_search");
162
+ expect(lastIpcCall?.params).toEqual({
163
+ queryParams: { q: "flux capacitor" },
164
+ });
165
+ });
166
+
167
+ test("passes --limit as a query param string", async () => {
168
+ mockIpcResult = {
169
+ ok: true,
170
+ result: { query: "flux", results: [] },
171
+ };
172
+ const { code } = await runSearch(["search", "flux", "--limit", "5"]);
173
+ expect(code).toBe(0);
174
+ expect(lastIpcCall?.params).toEqual({
175
+ queryParams: { q: "flux", limit: "5" },
176
+ });
177
+ });
178
+
179
+ test("prints conversation rows and excerpts in human mode", async () => {
180
+ mockIpcResult = {
181
+ ok: true,
182
+ result: { query: "flux", results: [matchingResult] },
183
+ };
184
+ const { code, stdout } = await runSearch(["search", "flux"]);
185
+ expect(code).toBe(0);
186
+ expect(stdout).toContain("conv-xyz");
187
+ expect(stdout).toContain("Quarterly metrics");
188
+ expect(stdout).toContain("3h ago");
189
+ expect(stdout).toContain("user let's review the flux capacitor numbers");
190
+ expect(stdout).toContain(
191
+ "assistant the Q3 flux capacitor spend is on track",
192
+ );
193
+ });
194
+
195
+ test("prints Untitled when the conversation title is null", async () => {
196
+ mockIpcResult = {
197
+ ok: true,
198
+ result: {
199
+ query: "flux",
200
+ results: [
201
+ {
202
+ ...matchingResult,
203
+ conversationTitle: null,
204
+ matchingMessages: [],
205
+ },
206
+ ],
207
+ },
208
+ };
209
+ const { code, stdout } = await runSearch(["search", "flux"]);
210
+ expect(code).toBe(0);
211
+ expect(stdout).toContain("Untitled");
212
+ });
213
+
214
+ test("prints a no-match line when the daemon returns no results", async () => {
215
+ mockIpcResult = {
216
+ ok: true,
217
+ result: { query: "no-such-term", results: [] },
218
+ };
219
+ const { code, stdout } = await runSearch(["search", "no-such-term"]);
220
+ expect(code).toBe(0);
221
+ expect(lastIpcCall?.method).toBe("conversations_search");
222
+ expect(stdout).toBe('No conversations matched "no-such-term"\n');
223
+ });
224
+
225
+ test("--json emits the daemon payload", async () => {
226
+ mockIpcResult = {
227
+ ok: true,
228
+ result: { query: "flux", results: [matchingResult] },
229
+ };
230
+ const { code, stdout } = await runSearch(["search", "flux", "--json"]);
231
+ expect(code).toBe(0);
232
+ expect(JSON.parse(stdout)).toEqual({
233
+ query: "flux",
234
+ results: [matchingResult],
235
+ });
236
+ });
237
+
238
+ test("--json reports IPC failure as { ok: false } and exits non-zero", async () => {
239
+ mockIpcResult = { ok: false, error: "assistant not running" };
240
+ const { code, stdout } = await runSearch(["search", "flux", "--json"]);
241
+ expect(code).toBe(10);
242
+ expect(JSON.parse(stdout)).toEqual({
243
+ ok: false,
244
+ error: "assistant not running",
245
+ });
246
+ });
247
+
248
+ test("rejects a whitespace-only term before IPC", async () => {
249
+ const { code, stderr } = await runSearch(["search", " "]);
250
+ expect(code).toBe(1);
251
+ expect(lastIpcCall).toBeNull();
252
+ expect(stderr).toContain("Search term must be a non-empty string");
253
+ });
254
+
255
+ test("--json rejects a whitespace-only term as { ok: false }", async () => {
256
+ const { code, stdout } = await runSearch(["search", " ", "--json"]);
257
+ expect(code).toBe(1);
258
+ expect(lastIpcCall).toBeNull();
259
+ expect(JSON.parse(stdout)).toMatchObject({
260
+ ok: false,
261
+ error: expect.stringContaining("Search term must be a non-empty string"),
262
+ });
263
+ });
264
+
265
+ test("rejects a non-numeric --limit before IPC", async () => {
266
+ const { code, stderr } = await runSearch([
267
+ "search",
268
+ "flux",
269
+ "--limit",
270
+ "all",
271
+ ]);
272
+ expect(code).toBe(1);
273
+ expect(lastIpcCall).toBeNull();
274
+ expect(stderr).toContain("Invalid --limit value");
275
+ });
276
+
277
+ test("search --help documents the term, --limit, and examples", async () => {
278
+ const { code, stdout } = await runSearch(["search", "--help"]);
279
+ expect(code).toBe(0);
280
+ expect(stdout).toContain(
281
+ "Search conversations by title and message content",
282
+ );
283
+ expect(stdout).toContain("--limit");
284
+ expect(stdout).toContain(
285
+ 'assistant conversations search "project planning"',
286
+ );
287
+ expect(stdout).toContain("--json");
288
+ });
289
+ });
@@ -98,6 +98,7 @@ mock.module("../../../ipc/gateway-client.js", () => ({
98
98
  }));
99
99
 
100
100
  mock.module("../../../messaging/providers/slack/send.js", () => ({
101
+ updateSlackMessage: async () => ({ ok: true, ts: "ts" }),
101
102
  sendSlackReply: async (
102
103
  chatId: string,
103
104
  text: string,
@@ -120,6 +120,24 @@ describe("providers create — derived auth", () => {
120
120
  });
121
121
  });
122
122
 
123
+ test("forwards optional --base-url for ollama create", async () => {
124
+ await run([
125
+ "providers",
126
+ "create",
127
+ "ollama-home",
128
+ "--provider",
129
+ "ollama",
130
+ "--base-url",
131
+ "http://192.168.1.50:11434/v1",
132
+ ]);
133
+ expect(lastIpcCall()?.params?.body).toEqual({
134
+ name: "ollama-home",
135
+ provider: "ollama",
136
+ auth: { type: "none" },
137
+ base_url: "http://192.168.1.50:11434/v1",
138
+ });
139
+ });
140
+
123
141
  test("rejects a keyed provider without --credential before calling the daemon", async () => {
124
142
  const { exitCode } = await run([
125
143
  "providers",
@@ -332,6 +350,56 @@ describe("providers update", () => {
332
350
  base_url: "http://localhost:5678/v1",
333
351
  });
334
352
  });
353
+
354
+ test("ollama update forwards --base-url after GET", async () => {
355
+ mockIpcResult = {
356
+ ok: true,
357
+ result: {
358
+ ...CONNECTION_RESULT,
359
+ name: "ollama",
360
+ provider: "ollama",
361
+ auth: { type: "none" },
362
+ },
363
+ };
364
+ await run([
365
+ "providers",
366
+ "update",
367
+ "ollama",
368
+ "--base-url",
369
+ "http://192.168.1.50:11434/v1",
370
+ ]);
371
+ expect(ipcCalls.map((c) => c.method)).toEqual([
372
+ "inference_provider_connections_get",
373
+ "inference_provider_connections_update",
374
+ ]);
375
+ expect(lastIpcCall()?.params).toEqual({
376
+ pathParams: { name: "ollama" },
377
+ body: {
378
+ auth: { type: "none" },
379
+ base_url: "http://192.168.1.50:11434/v1",
380
+ },
381
+ });
382
+ });
383
+ });
384
+
385
+ describe("providers get output", () => {
386
+ test("prints base_url when the connection stores one", async () => {
387
+ mockIpcResult = {
388
+ ok: true,
389
+ result: {
390
+ name: "ollama",
391
+ provider: "ollama",
392
+ auth: { type: "none" },
393
+ baseUrl: "http://192.168.1.50:11434/v1",
394
+ createdAt: 0,
395
+ updatedAt: 0,
396
+ },
397
+ };
398
+ const { stdout } = await run(["providers", "get", "ollama"]);
399
+ expect(stdout).toContain("name: ollama");
400
+ expect(stdout).toContain("provider: ollama");
401
+ expect(stdout).toContain("base_url: http://192.168.1.50:11434/v1");
402
+ });
335
403
  });
336
404
 
337
405
  describe("providers list output", () => {
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Tests for the keys set CLI subcommand's agent-shell guard.
2
+ * Tests for the keys CLI subcommands: the set agent-shell guard and the
3
+ * delete force flag.
3
4
  *
4
5
  * Validates:
5
6
  * - inline set from an agent shell (__CONVERSATION_ID) is refused with a
@@ -8,6 +9,8 @@
8
9
  * - --generated bypasses the guard and stores via the daemon client
9
10
  * - a plain user-terminal invocation (no markers) stores via the daemon
10
11
  * client
12
+ * - delete forwards the caller's force choice to the daemon client
13
+ * - an in-use refusal points at the --force escape hatch
11
14
  */
12
15
 
13
16
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
@@ -18,7 +21,16 @@ import { Command } from "commander";
18
21
  // Mock state
19
22
  // ---------------------------------------------------------------------------
20
23
 
24
+ interface DeleteResultStub {
25
+ result: "deleted" | "not-found" | "error";
26
+ error?: string;
27
+ code?: string;
28
+ }
29
+
21
30
  let mockSetSecureKeyViaDaemon = mock(() => Promise.resolve({ ok: true }));
31
+ let mockDeleteSecureKeyViaDaemon = mock(
32
+ (): Promise<DeleteResultStub> => Promise.resolve({ result: "deleted" }),
33
+ );
22
34
 
23
35
  // ---------------------------------------------------------------------------
24
36
  // Mocks — must be declared before importing the module under test
@@ -26,11 +38,11 @@ let mockSetSecureKeyViaDaemon = mock(() => Promise.resolve({ ok: true }));
26
38
 
27
39
  mock.module("../../lib/daemon-credential-client.js", () => ({
28
40
  setSecureKeyViaDaemon: mockSetSecureKeyViaDaemon,
29
- deleteSecureKeyViaDaemon: mock(() =>
30
- Promise.resolve({ result: "deleted" as const }),
31
- ),
41
+ deleteSecureKeyViaDaemon: mockDeleteSecureKeyViaDaemon,
32
42
  }));
33
43
 
44
+ const loggedErrors: string[] = [];
45
+
34
46
  mock.module("../../../util/logger.js", () => ({
35
47
  getLogger: () => ({
36
48
  info: () => {},
@@ -41,7 +53,9 @@ mock.module("../../../util/logger.js", () => ({
41
53
  getCliLogger: () => ({
42
54
  info: () => {},
43
55
  warn: () => {},
44
- error: () => {},
56
+ error: (message: unknown) => {
57
+ loggedErrors.push(String(message));
58
+ },
45
59
  debug: () => {},
46
60
  }),
47
61
  }));
@@ -55,6 +69,10 @@ let savedSkillContextJson: string | undefined;
55
69
 
56
70
  beforeEach(() => {
57
71
  mockSetSecureKeyViaDaemon = mock(() => Promise.resolve({ ok: true }));
72
+ mockDeleteSecureKeyViaDaemon = mock(
73
+ (): Promise<DeleteResultStub> => Promise.resolve({ result: "deleted" }),
74
+ );
75
+ loggedErrors.length = 0;
58
76
  savedConversationId = process.env.__CONVERSATION_ID;
59
77
  savedSkillContextJson = process.env.__SKILL_CONTEXT_JSON;
60
78
  delete process.env.__CONVERSATION_ID;
@@ -83,9 +101,7 @@ afterEach(() => {
83
101
  async function runKeysCommand(...args: string[]) {
84
102
  mock.module("../../lib/daemon-credential-client.js", () => ({
85
103
  setSecureKeyViaDaemon: mockSetSecureKeyViaDaemon,
86
- deleteSecureKeyViaDaemon: mock(() =>
87
- Promise.resolve({ result: "deleted" as const }),
88
- ),
104
+ deleteSecureKeyViaDaemon: mockDeleteSecureKeyViaDaemon,
89
105
  }));
90
106
 
91
107
  const { registerKeysCommand } = await import("../keys.js");
@@ -187,3 +203,68 @@ describe("assistant keys set (agent-shell guard)", () => {
187
203
  expect(process.exitCode).toBe(0);
188
204
  });
189
205
  });
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // delete — in-use guard and --force
209
+ // ---------------------------------------------------------------------------
210
+
211
+ describe("assistant keys delete (in-use guard)", () => {
212
+ test("deletes without force by default", async () => {
213
+ // GIVEN a stored key no connection depends on
214
+ // WHEN the key is deleted without --force
215
+ await runKeysCommand("keys", "delete", "acme");
216
+
217
+ // THEN the daemon is asked for a non-forced delete
218
+ expect(mockDeleteSecureKeyViaDaemon).toHaveBeenCalledTimes(1);
219
+ const [type, provider, force] = mockDeleteSecureKeyViaDaemon.mock
220
+ .calls[0] as unknown as [string, string, boolean];
221
+ expect(type).toBe("api_key");
222
+ expect(provider).toBe("acme");
223
+ expect(force).toBe(false);
224
+ expect(process.exitCode).toBe(0);
225
+ });
226
+
227
+ test("--force carries explicit intent to the daemon", async () => {
228
+ // GIVEN a caller who accepts breaking dependent connections
229
+ // WHEN --force is passed
230
+ await runKeysCommand("keys", "delete", "acme", "--force");
231
+
232
+ // THEN the daemon receives the force flag
233
+ const [, , force] = mockDeleteSecureKeyViaDaemon.mock
234
+ .calls[0] as unknown as [string, string, boolean];
235
+ expect(force).toBe(true);
236
+ });
237
+
238
+ test("names dependent connections and points at --force when refused", async () => {
239
+ // GIVEN a key an LLM provider connection resolves its auth through
240
+ mockDeleteSecureKeyViaDaemon = mock(
241
+ (): Promise<DeleteResultStub> =>
242
+ Promise.resolve({
243
+ result: "error",
244
+ code: "CREDENTIAL_IN_USE",
245
+ error:
246
+ 'Credential credential/acme/api_key is in use by connection "acme-router".',
247
+ }),
248
+ );
249
+ const exitCalls: number[] = [];
250
+ const origExit = process.exit;
251
+ process.exit = ((code?: number) => {
252
+ exitCalls.push(code ?? 0);
253
+ throw new Error("__exit__");
254
+ }) as typeof process.exit;
255
+
256
+ // WHEN the delete is attempted without --force
257
+ try {
258
+ await runKeysCommand("keys", "delete", "acme");
259
+ } catch (err) {
260
+ expect((err as Error).message).toBe("__exit__");
261
+ } finally {
262
+ process.exit = origExit;
263
+ }
264
+
265
+ // THEN the refusal names the connection and suggests the escape hatch
266
+ expect(exitCalls).toEqual([1]);
267
+ expect(loggedErrors.join("\n")).toContain('connection "acme-router"');
268
+ expect(loggedErrors.join("\n")).toContain("keys delete acme --force");
269
+ });
270
+ });