@vellumai/assistant 0.11.4 → 0.11.5-staging.2

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 (367) 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__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -44,6 +44,31 @@ export function canActOnPrivilegedDocuments(actor: {
44
44
  );
45
45
  }
46
46
 
47
+ /**
48
+ * Whether personal-memory content (memory pages, PKB, matched v3 card
49
+ * sections, the v2 static block) may be surfaced to an actor.
50
+ *
51
+ * The trust class decides, and nothing else. Personal memory is elevated
52
+ * access, and every other consumer of "no actor was bound" already fails
53
+ * closed for it: `FALLBACK_TURN_TRUST` is documented as low-trust and "never
54
+ * guardian", and the workflow resume path deliberately declines to fall back
55
+ * to the guardian context so a resumed run cannot gain trust it did not start
56
+ * with. A gate that granted memory for the same condition would contradict
57
+ * them.
58
+ *
59
+ * Notably this takes no channel. A surface is not an actor: arriving on the
60
+ * first-party console says nothing about who is asking, and an actor whose
61
+ * class could not be resolved fails closed to `unknown` upstream. Answering
62
+ * "who is this" is `resolveTrustClass`'s job -- including the local/native
63
+ * turns that resolve to guardian under a disabled-auth posture -- and this
64
+ * function only answers what that actor may see.
65
+ */
66
+ export function canSeePersonalMemory(actor: {
67
+ trustClass: RawTrustClass;
68
+ }): boolean {
69
+ return resolveCapabilities(actor.trustClass).canAccessMemory;
70
+ }
71
+
47
72
  /**
48
73
  * Whether an archive-by-sender invocation is authorized. Any one of a surface
49
74
  * action, a task-batch authorization, or an explicit prompt approval suffices.
@@ -97,8 +97,12 @@ export interface GuardianReplyContext {
97
97
  channel: string;
98
98
  /** Actor identity context for the sender. */
99
99
  actor: ActorContext;
100
- /** Conversation ID for this message (may be the guardian's conversation). */
101
- conversationId: string;
100
+ /**
101
+ * Conversation the message arrived in (may be the guardian's own). Only the
102
+ * text and NL paths read it; reactions and `apr:` callbacks resolve their
103
+ * target by card address or embedded request id and may pass none.
104
+ */
105
+ conversationId?: string;
102
106
  /** Callback data from button presses (e.g. `apr:<requestId>:<action>`). */
103
107
  callbackData?: string;
104
108
  /**
@@ -17,6 +17,7 @@
17
17
  */
18
18
  export type HttpErrorCode =
19
19
  | "BAD_REQUEST"
20
+ | "CREDENTIAL_IN_USE"
20
21
  | "UNAUTHORIZED"
21
22
  | "FORBIDDEN"
22
23
  | "NOT_FOUND"
@@ -27,6 +27,7 @@ import {
27
27
  isDbMigrationGateBypassed,
28
28
  } from "../daemon/daemon-readiness.js";
29
29
  import { processMessage } from "../daemon/process-message.js";
30
+ import { makeAddrInUseError } from "../daemon/startup-error.js";
30
31
  import {
31
32
  createLiveVoiceConnection,
32
33
  type LiveVoiceConnection,
@@ -975,9 +976,14 @@ let instance: RuntimeHttpServer | null = null;
975
976
 
976
977
  /**
977
978
  * Start the runtime HTTP server singleton early in daemon startup so /healthz
978
- * answers ASAP. A bind failure (port in use, permission denied, fd exhaustion)
979
- * is non-fatal: it is logged and the daemon falls back to IPC-only operation,
980
- * leaving the singleton unset.
979
+ * answers ASAP.
980
+ *
981
+ * An occupied address (`EADDRINUSE`) aborts startup: whatever holds the port
982
+ * owns every HTTP client of this workspace, including the gateway proxy that
983
+ * fronts `/v1/*`. Continuing would leave a daemon that answers IPC (so `vellum
984
+ * ps` and platform status read healthy) while every proxied HTTP route 502s
985
+ * against a foreign listener. Any other bind failure (permission denied, fd
986
+ * exhaustion) is non-fatal, logged with the singleton left unset.
981
987
  */
982
988
  export async function startRuntimeHttpServer(): Promise<void> {
983
989
  const port = getRuntimeHttpPort();
@@ -992,11 +998,20 @@ export async function startRuntimeHttpServer(): Promise<void> {
992
998
  "Daemon startup: runtime HTTP server listening",
993
999
  );
994
1000
  } catch (err) {
1001
+ if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") {
1002
+ log.error(
1003
+ { err, port, hostname },
1004
+ "Runtime HTTP port already in use, aborting startup to avoid an HTTP-blind daemon",
1005
+ );
1006
+ throw makeAddrInUseError(
1007
+ `Runtime HTTP port ${hostname}:${port} is already in use by another process. Stop it, or set RUNTIME_HTTP_PORT to a free port.`,
1008
+ err,
1009
+ );
1010
+ }
995
1011
  log.warn(
996
1012
  { err, port },
997
1013
  "Failed to start runtime HTTP server, continuing without it",
998
1014
  );
999
- instance = null;
1000
1015
  }
1001
1016
  }
1002
1017
 
@@ -1007,7 +1022,7 @@ export async function startRuntimeHttpServer(): Promise<void> {
1007
1022
  * expiry, profile reaping) must still run; the retry sweep additionally skips
1008
1023
  * its cycles while readiness is unready. Never called before migrations settle,
1009
1024
  * so the sweeps can't race a schema mid-migration. No-op if the HTTP server
1010
- * failed to bind (IPC-only mode) or sweeps already started.
1025
+ * isn't running or sweeps already started.
1011
1026
  */
1012
1027
  export function startRuntimeHttpServerBackgroundSweeps(): void {
1013
1028
  instance?.startBackgroundSweeps();
@@ -333,3 +333,153 @@ describe("report_client_presence route", () => {
333
333
  ).toBeUndefined();
334
334
  });
335
335
  });
336
+
337
+ describe("report_web_presence route", () => {
338
+ beforeEach(() => {
339
+ fakeHttpAuthDisabled = false;
340
+ clearHubClients(assistantEventHub);
341
+ });
342
+
343
+ function registerWebClient(args: {
344
+ clientId: string;
345
+ actorPrincipalId?: string;
346
+ }): void {
347
+ registerHubClient({
348
+ hub: assistantEventHub,
349
+ clientId: args.clientId,
350
+ interfaceId: "web",
351
+ actorPrincipalId: args.actorPrincipalId,
352
+ });
353
+ }
354
+
355
+ test("records visibility and focused conversation against the client named by the header", () => {
356
+ registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
357
+
358
+ const handler = findHandler("report_web_presence");
359
+ const result = handler({
360
+ headers: {
361
+ "x-vellum-client-id": "client-A1",
362
+ "x-vellum-actor-principal-id": "user-A",
363
+ },
364
+ body: { visible: true, focusedConversationId: "conv-1" },
365
+ }) as { recorded: boolean };
366
+
367
+ expect(result).toEqual({ recorded: true });
368
+ expect(
369
+ assistantEventHub.getClientById("client-A1")?.webPresence,
370
+ ).toMatchObject({
371
+ visible: true,
372
+ focusedConversationId: "conv-1",
373
+ });
374
+ });
375
+
376
+ test("accepts a null focusedConversationId", () => {
377
+ registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
378
+
379
+ const handler = findHandler("report_web_presence");
380
+ const result = handler({
381
+ headers: {
382
+ "x-vellum-client-id": "client-A1",
383
+ "x-vellum-actor-principal-id": "user-A",
384
+ },
385
+ body: { visible: false, focusedConversationId: null },
386
+ }) as { recorded: boolean };
387
+
388
+ expect(result).toEqual({ recorded: true });
389
+ expect(
390
+ assistantEventHub.getClientById("client-A1")?.webPresence,
391
+ ).toMatchObject({
392
+ visible: false,
393
+ focusedConversationId: null,
394
+ });
395
+ });
396
+
397
+ test("returns recorded false when the reported client is not connected", () => {
398
+ const handler = findHandler("report_web_presence");
399
+ const result = handler({
400
+ headers: {
401
+ "x-vellum-client-id": "client-gone",
402
+ "x-vellum-actor-principal-id": "user-A",
403
+ },
404
+ body: { visible: true, focusedConversationId: "conv-1" },
405
+ }) as { recorded: boolean };
406
+
407
+ expect(result).toEqual({ recorded: false });
408
+ });
409
+
410
+ test("returns recorded false when the caller does not own the client", () => {
411
+ registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
412
+
413
+ const handler = findHandler("report_web_presence");
414
+ const result = handler({
415
+ headers: {
416
+ "x-vellum-client-id": "client-A1",
417
+ "x-vellum-actor-principal-id": "user-B",
418
+ },
419
+ body: { visible: true, focusedConversationId: "conv-1" },
420
+ }) as { recorded: boolean };
421
+
422
+ expect(result).toEqual({ recorded: false });
423
+ expect(
424
+ assistantEventHub.getClientById("client-A1")?.webPresence,
425
+ ).toBeUndefined();
426
+ });
427
+
428
+ test("returns recorded false for a client with no stored actorPrincipalId", () => {
429
+ registerWebClient({ clientId: "client-noprincipal" });
430
+
431
+ const handler = findHandler("report_web_presence");
432
+ const result = handler({
433
+ headers: {
434
+ "x-vellum-client-id": "client-noprincipal",
435
+ "x-vellum-actor-principal-id": "user-A",
436
+ },
437
+ body: { visible: true, focusedConversationId: "conv-1" },
438
+ }) as { recorded: boolean };
439
+
440
+ expect(result).toEqual({ recorded: false });
441
+ expect(
442
+ assistantEventHub.getClientById("client-noprincipal")?.webPresence,
443
+ ).toBeUndefined();
444
+ });
445
+
446
+ test("dev-bypass mode records presence without an ownership check", () => {
447
+ fakeHttpAuthDisabled = true;
448
+ registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
449
+
450
+ const handler = findHandler("report_web_presence");
451
+ const result = handler({
452
+ headers: {
453
+ "x-vellum-client-id": "client-A1",
454
+ "x-vellum-actor-principal-id": "user-B",
455
+ },
456
+ body: { visible: true, focusedConversationId: "conv-1" },
457
+ }) as { recorded: boolean };
458
+
459
+ expect(result).toEqual({ recorded: true });
460
+ });
461
+
462
+ test("throws BadRequestError when the client-id header is missing", () => {
463
+ const handler = findHandler("report_web_presence");
464
+
465
+ expect(() =>
466
+ handler({ body: { visible: true, focusedConversationId: "conv-1" } }),
467
+ ).toThrow(BadRequestError);
468
+ });
469
+
470
+ test("throws BadRequestError for a malformed body", () => {
471
+ registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
472
+
473
+ const handler = findHandler("report_web_presence");
474
+
475
+ expect(() =>
476
+ handler({
477
+ headers: {
478
+ "x-vellum-client-id": "client-A1",
479
+ "x-vellum-actor-principal-id": "user-A",
480
+ },
481
+ body: { visible: "yes", focusedConversationId: "conv-1" },
482
+ }),
483
+ ).toThrow(BadRequestError);
484
+ });
485
+ });
@@ -683,6 +683,139 @@ describe("GET /v1/conversations with needsAttention", () => {
683
683
  });
684
684
  });
685
685
 
686
+ describe("GET /v1/conversations with foregroundOnly", () => {
687
+ function fileIntoGroup(conversationId: string, groupId: string): void {
688
+ rawRun(
689
+ "test:fileIntoGroup",
690
+ "UPDATE conversations SET group_id = ? WHERE id = ?",
691
+ groupId,
692
+ conversationId,
693
+ );
694
+ }
695
+
696
+ function surface(conversationId: string): void {
697
+ rawRun(
698
+ "test:surfaceConversation",
699
+ "UPDATE conversations SET surfaced_at = ? WHERE id = ?",
700
+ Date.now(),
701
+ conversationId,
702
+ );
703
+ }
704
+
705
+ beforeEach(() => {
706
+ clearConversations();
707
+ });
708
+
709
+ test("drops the automated runs the standard listing admits through a custom group", async () => {
710
+ /* The standard listing shows a background run filed in a custom group,
711
+ so a caller reading page one to find "the newest chat" can lead with
712
+ one; the filter is what lets that caller ask for chats alone. A
713
+ surfaced run is a chat for this purpose, exactly as the unread count
714
+ treats it. */
715
+ const group = createGroup("Work");
716
+ createConversation("plain-chat");
717
+ const bgInGroup = createConversation({
718
+ title: "bg-in-group",
719
+ conversationType: "background",
720
+ });
721
+ fileIntoGroup(bgInGroup.id, group.id);
722
+ const surfacedBg = createConversation({
723
+ title: "surfaced-bg",
724
+ conversationType: "background",
725
+ });
726
+ surface(surfacedBg.id);
727
+
728
+ const unfiltered = await invoke();
729
+ expect(unfiltered.conversations.map((c) => c.title).sort()).toEqual([
730
+ "bg-in-group",
731
+ "plain-chat",
732
+ "surfaced-bg",
733
+ ]);
734
+
735
+ const result = await invoke({ foregroundOnly: "true" });
736
+ expect(result.conversations.map((c) => c.title).sort()).toEqual([
737
+ "plain-chat",
738
+ "surfaced-bg",
739
+ ]);
740
+ });
741
+
742
+ test("hasMore and the total describe the filtered set, not the whole table", async () => {
743
+ /* The reason for the filter is a limit=1 read whose first row is the
744
+ answer; that read is only right if the page and its total come from
745
+ the same predicate. */
746
+ const group = createGroup("Work");
747
+ for (let i = 0; i < 3; i++) {
748
+ const bg = createConversation({
749
+ title: `bg-${i}`,
750
+ conversationType: "background",
751
+ });
752
+ fileIntoGroup(bg.id, group.id);
753
+ }
754
+ createConversation("the-only-chat");
755
+
756
+ const page = await invoke({ foregroundOnly: "true", limit: "1" });
757
+
758
+ expect(page.conversations.map((c) => c.title)).toEqual(["the-only-chat"]);
759
+ expect(page.hasMore).toBe(false);
760
+ });
761
+
762
+ test("a foreground-only first page has no pinned rows appended to it", async () => {
763
+ /* Same rule as the attention- and group-scoped pages: the injection is
764
+ for a client reading Pinned out of the unfiltered list, and a caller
765
+ that asked for the newest chat in one row is not that client. The
766
+ older pinned chat is a member of the filter, so only the injection
767
+ could put it on a limit=1 page. */
768
+ const older = createConversation("older-pinned-chat");
769
+ rawRun(
770
+ "test:pinConversation",
771
+ "UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned', last_message_at = ? WHERE id = ?",
772
+ Date.now() - 60_000,
773
+ older.id,
774
+ );
775
+ const newer = createConversation("newest-chat");
776
+ rawRun(
777
+ "test:bumpRecency",
778
+ "UPDATE conversations SET last_message_at = ? WHERE id = ?",
779
+ Date.now(),
780
+ newer.id,
781
+ );
782
+
783
+ const unfiltered = await invoke({ limit: "1" });
784
+ expect(unfiltered.conversations.map((c) => c.title)).toEqual([
785
+ "newest-chat",
786
+ "older-pinned-chat",
787
+ ]);
788
+
789
+ const result = await invoke({ foregroundOnly: "true", limit: "1" });
790
+ expect(result.conversations.map((c) => c.title)).toEqual(["newest-chat"]);
791
+ });
792
+
793
+ test("composes with the other filters", async () => {
794
+ const group = createGroup("Work");
795
+ const chatInGroup = createConversation("chat-in-group");
796
+ fileIntoGroup(chatInGroup.id, group.id);
797
+ const bgInGroup = createConversation({
798
+ title: "bg-in-group",
799
+ conversationType: "background",
800
+ });
801
+ fileIntoGroup(bgInGroup.id, group.id);
802
+ createConversation("chat-outside");
803
+
804
+ const result = await invoke({
805
+ foregroundOnly: "true",
806
+ groupId: group.id,
807
+ });
808
+
809
+ expect(result.conversations.map((c) => c.title)).toEqual(["chat-in-group"]);
810
+ });
811
+
812
+ test('any value other than "true" is rejected with a 400', () => {
813
+ for (const bad of ["false", "1", "yes", "TRUE"]) {
814
+ expect(() => invoke({ foregroundOnly: bad })).toThrow(BadRequestError);
815
+ }
816
+ });
817
+ });
818
+
686
819
  describe("GET /v1/conversations/unread-count", () => {
687
820
  const unreadCountHandler = findHandler(
688
821
  CONVERSATION_LIST_ROUTES,
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Deleting a credential an LLM connection dispatches with must be refused
3
+ * until the caller confirms, because losing it silently takes that connection
4
+ * offline: the adapter keeps routing to the provider with no key and the turn
5
+ * comes back empty.
6
+ */
7
+
8
+ import { beforeEach, describe, expect, test } from "bun:test";
9
+
10
+ import { getDb } from "../../../persistence/db-connection.js";
11
+ import { initializeDb } from "../../../persistence/db-init.js";
12
+ import { providerConnections } from "../../../persistence/schema/inference.js";
13
+ import { createConnection } from "../../../providers/inference/connections.js";
14
+ import { credentialKey } from "../../../security/credential-key.js";
15
+ import {
16
+ deleteSecureKeyAsync,
17
+ getSecureKeyAsync,
18
+ setSecureKeyAsync,
19
+ } from "../../../security/secure-keys.js";
20
+ import { CredentialInUseError } from "../credential-in-use.js";
21
+ import { ROUTES as CREDENTIAL_ROUTES } from "../credential-routes.js";
22
+ import { ROUTES as SECRET_ROUTES } from "../secret-routes.js";
23
+ import type { RouteDefinition } from "../types.js";
24
+
25
+ await initializeDb();
26
+
27
+ const SERVICE = "agentrouter";
28
+ const FIELD = "api_key";
29
+ const ACCOUNT = credentialKey(SERVICE, FIELD);
30
+
31
+ function findHandler(
32
+ routes: RouteDefinition[],
33
+ operationId: string,
34
+ ): RouteDefinition["handler"] {
35
+ const route = routes.find(
36
+ (candidate) => candidate.operationId === operationId,
37
+ );
38
+ if (!route) {
39
+ throw new Error(`Route ${operationId} not found`);
40
+ }
41
+ return route.handler;
42
+ }
43
+
44
+ const deleteCredential = findHandler(CREDENTIAL_ROUTES, "credentials_delete");
45
+ const deleteSecret = findHandler(SECRET_ROUTES, "secrets_delete");
46
+
47
+ /** Run a handler and hand back whatever it rejected with, or null on success. */
48
+ async function rejection(run: () => unknown): Promise<unknown> {
49
+ try {
50
+ await run();
51
+ return null;
52
+ } catch (error) {
53
+ return error;
54
+ }
55
+ }
56
+
57
+ /** Point an api_key connection at the credential under test. */
58
+ function createConnectionUsing(name: string, credential: string): void {
59
+ const result = createConnection(getDb(), {
60
+ name,
61
+ provider: "openai",
62
+ auth: { type: "api_key", credential },
63
+ });
64
+ if (!result.ok) {
65
+ throw new Error(`Failed to create connection ${name}`);
66
+ }
67
+ }
68
+
69
+ beforeEach(async () => {
70
+ getDb().delete(providerConnections).run();
71
+ await deleteSecureKeyAsync(ACCOUNT);
72
+ });
73
+
74
+ describe("credential delete in-use guard", () => {
75
+ test("refuses a delete while a connection resolves auth through the credential", async () => {
76
+ // GIVEN a stored credential
77
+ await setSecureKeyAsync(ACCOUNT, "sk-test-value");
78
+
79
+ // AND two LLM connections that dispatch with it
80
+ createConnectionUsing("router-primary", `${SERVICE}:${FIELD}`);
81
+ createConnectionUsing("router-fallback", ACCOUNT);
82
+
83
+ // WHEN the credential is deleted without confirmation
84
+ const error = await rejection(() =>
85
+ deleteCredential({ body: { service: SERVICE, field: FIELD } }),
86
+ );
87
+
88
+ // THEN the delete is refused as a client error naming the connections
89
+ expect(error).toBeInstanceOf(CredentialInUseError);
90
+ const refusal = error as CredentialInUseError;
91
+ expect(refusal.statusCode).toBe(400);
92
+ expect(refusal.code).toBe("CREDENTIAL_IN_USE");
93
+ expect(refusal.connections).toEqual(["router-fallback", "router-primary"]);
94
+ expect(refusal.message).toContain("router-primary");
95
+
96
+ // AND the credential is left in place, so those connections keep working
97
+ expect(await getSecureKeyAsync(ACCOUNT)).toBe("sk-test-value");
98
+ });
99
+
100
+ test("deletes the credential when the caller confirms with force", async () => {
101
+ // GIVEN a stored credential a connection dispatches with
102
+ await setSecureKeyAsync(ACCOUNT, "sk-test-value");
103
+ createConnectionUsing("router-primary", `${SERVICE}:${FIELD}`);
104
+
105
+ // WHEN the delete is retried with force
106
+ const result = await deleteCredential({
107
+ body: { service: SERVICE, field: FIELD, force: true },
108
+ });
109
+
110
+ // THEN it succeeds and reports the connections it took offline
111
+ expect(result).toMatchObject({
112
+ service: SERVICE,
113
+ field: FIELD,
114
+ affectedConnections: ["router-primary"],
115
+ });
116
+
117
+ // AND the credential is gone from secure storage
118
+ expect(await getSecureKeyAsync(ACCOUNT)).toBeUndefined();
119
+ });
120
+
121
+ test("deletes an unreferenced credential without confirmation", async () => {
122
+ // GIVEN a stored credential no connection references
123
+ await setSecureKeyAsync(ACCOUNT, "sk-test-value");
124
+ createConnectionUsing("other-connection", "credential/openai/api_key");
125
+
126
+ // WHEN it is deleted without force
127
+ const result = await deleteCredential({
128
+ body: { service: SERVICE, field: FIELD },
129
+ });
130
+
131
+ // THEN the delete goes through with no affected connections
132
+ expect(result).toMatchObject({ affectedConnections: [] });
133
+ expect(await getSecureKeyAsync(ACCOUNT)).toBeUndefined();
134
+ });
135
+
136
+ test("guards the api-key delete surface too", async () => {
137
+ // GIVEN a provider API key a connection dispatches with
138
+ const apiKeyAccount = credentialKey("openrouter", FIELD);
139
+ await setSecureKeyAsync(apiKeyAccount, "sk-test-value");
140
+ createConnectionUsing("router-primary", apiKeyAccount);
141
+
142
+ // WHEN it is deleted through the secrets route without confirmation
143
+ const error = await rejection(() =>
144
+ deleteSecret({ body: { type: "api_key", name: "openrouter" } }),
145
+ );
146
+
147
+ // THEN the same refusal applies and the key survives
148
+ expect(error).toBeInstanceOf(CredentialInUseError);
149
+ expect((error as CredentialInUseError).connections).toEqual([
150
+ "router-primary",
151
+ ]);
152
+ expect(await getSecureKeyAsync(apiKeyAccount)).toBe("sk-test-value");
153
+ await deleteSecureKeyAsync(apiKeyAccount);
154
+ });
155
+ });
@@ -313,8 +313,59 @@ describe("POST inference/provider-connections (create)", () => {
313
313
  auth: { type: "none" },
314
314
  },
315
315
  },
316
- )) as { auth: object };
316
+ )) as { auth: object; baseUrl: string | null };
317
+ expect(result.auth).toEqual({ type: "none" });
318
+ expect(result.baseUrl).toBeNull();
319
+ });
320
+
321
+ test("creates an ollama connection with an optional base_url", async () => {
322
+ const result = (await call(
323
+ findHandler("inference_provider_connections_create"),
324
+ {
325
+ body: {
326
+ name: "ollama-remote",
327
+ provider: "ollama",
328
+ auth: { type: "none" },
329
+ base_url: "http://192.168.1.50:11434/v1",
330
+ },
331
+ },
332
+ )) as { auth: object; baseUrl: string | null };
317
333
  expect(result.auth).toEqual({ type: "none" });
334
+ expect(result.baseUrl).toBe("http://192.168.1.50:11434/v1");
335
+ });
336
+
337
+ test("updates an ollama connection base_url and can clear it", async () => {
338
+ await call(findHandler("inference_provider_connections_create"), {
339
+ body: {
340
+ name: "ollama-editable",
341
+ provider: "ollama",
342
+ auth: { type: "none" },
343
+ },
344
+ });
345
+
346
+ const updated = (await call(
347
+ findHandler("inference_provider_connections_update"),
348
+ {
349
+ pathParams: { name: "ollama-editable" },
350
+ body: {
351
+ auth: { type: "none" },
352
+ base_url: "http://127.0.0.1:11434/v1",
353
+ },
354
+ },
355
+ )) as { baseUrl: string | null };
356
+ expect(updated.baseUrl).toBe("http://127.0.0.1:11434/v1");
357
+
358
+ const cleared = (await call(
359
+ findHandler("inference_provider_connections_update"),
360
+ {
361
+ pathParams: { name: "ollama-editable" },
362
+ body: {
363
+ auth: { type: "none" },
364
+ base_url: null,
365
+ },
366
+ },
367
+ )) as { baseUrl: string | null };
368
+ expect(cleared.baseUrl).toBeNull();
318
369
  });
319
370
 
320
371
  test("derives api_key auth from provider + credential when auth is omitted", async () => {
@@ -148,6 +148,7 @@ describe("monitoring_status", () => {
148
148
  memoryStat: {
149
149
  anonBytes: 4 * 1024 * 1024 * 1024,
150
150
  fileBytes: 1024 * 1024 * 1024,
151
+ inactiveFileBytes: 768 * 1024 * 1024,
151
152
  kernelBytes: 512 * 1024 * 1024,
152
153
  slabReclaimableBytes: 400 * 1024 * 1024,
153
154
  slabUnreclaimableBytes: 100 * 1024 * 1024,