@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
@@ -69,17 +69,28 @@ mock.module("../../../contacts/contact-store.js", () => ({
69
69
  // Downstream side-effect stubs
70
70
  // ---------------------------------------------------------------------------
71
71
 
72
- let recordInboundCalls = 0;
72
+ let recordInboundCalls: Array<{ conversationId?: string }> = [];
73
+ let recordedEvent: { eventId: string; conversationId: string } | null = null;
74
+ let outboundTargetConversationId: string | null = null;
75
+ let storedTarget: { messageId: string; conversationId: string } | null = null;
73
76
  mock.module("../../../persistence/delivery-crud.js", () => ({
74
- recordInbound: () => {
75
- recordInboundCalls++;
77
+ recordInbound: (
78
+ _channel: string,
79
+ _chat: string,
80
+ _externalMessageId: string,
81
+ options?: { conversationId?: string },
82
+ ) => {
83
+ recordInboundCalls.push({ conversationId: options?.conversationId });
76
84
  return {
77
85
  eventId: "evt-1",
78
- conversationId: "conv-1",
86
+ conversationId: options?.conversationId ?? "conv-minted",
79
87
  accepted: true,
80
88
  duplicate: false,
81
89
  };
82
90
  },
91
+ findMessageBySourceId: () => storedTarget,
92
+ findSlackConversationByMessageTs: () => outboundTargetConversationId,
93
+ findInboundEvent: () => recordedEvent,
83
94
  clearPayload: () => {},
84
95
  linkMessage: () => {},
85
96
  }));
@@ -184,7 +195,6 @@ function buildParams(overrides: {
184
195
  ? { trustVerdict: overrides.trustVerdict }
185
196
  : {}),
186
197
  } as never,
187
- slackChannelName: "general",
188
198
  approvalConversationGenerator: undefined,
189
199
  };
190
200
  }
@@ -193,7 +203,7 @@ function expectDropped(result: Record<string, unknown>): void {
193
203
  expect(result.reaction).toBe("dropped_unknown_actor");
194
204
  // Dropped before any write or routing: no dedup record, no transcript row,
195
205
  // no approval-pipeline dispatch.
196
- expect(recordInboundCalls).toBe(0);
206
+ expect(recordInboundCalls.length).toBe(0);
197
207
  expect(addMessageCalls).toBe(0);
198
208
  expect(guardianReplyCalls.length).toBe(0);
199
209
  }
@@ -204,10 +214,13 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
204
214
  guardianDeliveryReads = 0;
205
215
  setMemberVerdictCalls = 0;
206
216
  contactLookups = 0;
207
- recordInboundCalls = 0;
217
+ recordInboundCalls = [];
218
+ recordedEvent = null;
219
+ outboundTargetConversationId = null;
208
220
  addMessageCalls = 0;
209
221
  guardianReplyCalls = [];
210
222
  guardianReplyResponse = undefined;
223
+ storedTarget = { messageId: "msg-target", conversationId: "conv-target" };
211
224
  });
212
225
 
213
226
  test("guardian verdict routes the reaction into the approval decision pipeline", async () => {
@@ -244,6 +257,77 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
244
257
  expect(result.accepted).toBe(true);
245
258
  expect(result.reaction).toBeUndefined();
246
259
  expect(addMessageCalls).toBe(1);
260
+ // Recorded into the reacted message's conversation, never a fresh one.
261
+ expect(recordInboundCalls).toEqual([{ conversationId: "conv-target" }]);
262
+ });
263
+
264
+ test("a reaction on the assistant's own post resolves through slackMeta", async () => {
265
+ // Outbound posts open no inbound event, so the ts is only on the row.
266
+ storedTarget = null;
267
+ outboundTargetConversationId = "conv-assistant-post";
268
+
269
+ const result = await handleSlackReactionIntercept(
270
+ buildParams({
271
+ rawSenderId: MEMBER_USER_ID,
272
+ trustVerdict: MEMBER_VERDICT,
273
+ }),
274
+ );
275
+
276
+ expect(recordInboundCalls).toEqual([
277
+ { conversationId: "conv-assistant-post" },
278
+ ]);
279
+ expect(addMessageCalls).toBe(1);
280
+ expect(result).toMatchObject({ accepted: true, duplicate: false });
281
+ });
282
+
283
+ test("a reaction on a message that is not stored is dropped without minting", async () => {
284
+ storedTarget = null;
285
+
286
+ const result = await handleSlackReactionIntercept(
287
+ buildParams({
288
+ rawSenderId: MEMBER_USER_ID,
289
+ trustVerdict: MEMBER_VERDICT,
290
+ }),
291
+ );
292
+
293
+ expect(result.reaction).toBe("dropped_unknown_target");
294
+ expect(recordInboundCalls.length).toBe(0);
295
+ expect(addMessageCalls).toBe(0);
296
+ });
297
+
298
+ test("a redelivered reaction never reaches the guardian rail twice", async () => {
299
+ recordedEvent = { eventId: "evt-1", conversationId: "conv-target" };
300
+
301
+ const result = await handleSlackReactionIntercept(
302
+ buildParams({
303
+ rawSenderId: GUARDIAN_USER_ID,
304
+ trustVerdict: GUARDIAN_VERDICT,
305
+ }),
306
+ );
307
+
308
+ expect(result).toEqual({
309
+ accepted: true,
310
+ duplicate: true,
311
+ eventId: "evt-1",
312
+ });
313
+ expect(guardianReplyCalls.length).toBe(0);
314
+ expect(addMessageCalls).toBe(0);
315
+ expect(recordInboundCalls.length).toBe(0);
316
+ });
317
+
318
+ test("a guardian card reaction still applies when the card is not a stored message", async () => {
319
+ storedTarget = null;
320
+ guardianReplyResponse = { accepted: true, canonicalRouter: "applied" };
321
+
322
+ const result = await handleSlackReactionIntercept(
323
+ buildParams({
324
+ rawSenderId: GUARDIAN_USER_ID,
325
+ trustVerdict: GUARDIAN_VERDICT,
326
+ }),
327
+ );
328
+
329
+ expect(result).toEqual(guardianReplyResponse);
330
+ expect(recordInboundCalls.length).toBe(0);
247
331
  });
248
332
 
249
333
  test("unknown verdict is dropped before any write", async () => {
@@ -9,7 +9,9 @@
9
9
  * or an access-request notification (LUM-2489),
10
10
  * - a stranger's reaction creates no conversation, binding, or transcript
11
11
  * row — it is dropped as channel noise,
12
- * - a known contact's reaction is recorded as an inline transcript signal,
12
+ * - a known contact's reaction is recorded as an inline transcript signal in
13
+ * the conversation of the message it was attached to, whether that message
14
+ * arrived from the channel or the assistant posted it,
13
15
  * - a guardian's reaction on an approval card is routed through the guardian
14
16
  * decision pipeline (the same path as buttons and text replies).
15
17
  *
@@ -28,12 +30,13 @@ import {
28
30
  } from "../../../messaging/providers/slack/message-metadata.js";
29
31
  import { addMessage } from "../../../persistence/conversation-crud.js";
30
32
  import {
31
- clearPayload,
33
+ findInboundEvent,
34
+ findMessageBySourceId,
35
+ findSlackConversationByMessageTs,
32
36
  linkMessage,
33
37
  recordInbound,
34
38
  } from "../../../persistence/delivery-crud.js";
35
39
  import { markProcessed } from "../../../persistence/delivery-status.js";
36
- import { upsertBinding } from "../../../persistence/external-conversation-store.js";
37
40
  import { getLogger } from "../../../util/logger.js";
38
41
  import type { ApprovalConversationGenerator } from "../../http-types.js";
39
42
  import {
@@ -105,8 +108,6 @@ export interface ReactionInterceptParams {
105
108
  actorUsername: string | undefined;
106
109
  replyCallbackUrl: string | undefined;
107
110
  sourceMetadata: SourceMetadata | undefined;
108
- /** Slack channel display name, for the conversation binding. */
109
- slackChannelName: string | null;
110
111
  approvalConversationGenerator: ApprovalConversationGenerator | undefined;
111
112
  }
112
113
 
@@ -130,7 +131,6 @@ export async function handleSlackReactionIntercept(
130
131
  actorUsername,
131
132
  replyCallbackUrl,
132
133
  sourceMetadata,
133
- slackChannelName,
134
134
  approvalConversationGenerator,
135
135
  } = params;
136
136
 
@@ -165,27 +165,11 @@ export async function handleSlackReactionIntercept(
165
165
  typeof sourceMetadata?.messageId === "string"
166
166
  ? sourceMetadata.messageId
167
167
  : undefined;
168
- const threadTs =
169
- typeof sourceMetadata?.threadId === "string" &&
170
- sourceMetadata.threadId.trim().length > 0
171
- ? sourceMetadata.threadId.trim()
172
- : undefined;
173
-
174
- // Record for dedup + conversation resolution (known contacts only — strangers
175
- // were dropped above).
176
- const result = recordInbound(
177
- sourceChannel,
178
- conversationExternalId,
179
- externalMessageId,
180
- {
181
- sourceMessageId: reactedMessageTs,
182
- sourceThreadId: threadTs,
183
- },
184
- );
185
-
186
168
  // Respect disk-pressure cleanup so reactions don't bypass storage
187
169
  // protection. Guardians resolve to `allow-cleanup-mode` (not `block`), so a
188
- // guardian's approval-by-reaction still flows.
170
+ // guardian's approval-by-reaction still flows. Blocked silently and before
171
+ // any write: a reaction is a passive signal, so the message pipeline's
172
+ // "storage is low, try again" notice is meaningless for an emoji.
189
173
  const diskPressure = classifyDiskPressureTurnPolicy(getDiskPressureStatus(), {
190
174
  sourceChannel,
191
175
  sourceInterface,
@@ -195,44 +179,39 @@ export async function handleSlackReactionIntercept(
195
179
  },
196
180
  });
197
181
  if (diskPressure.action === "block") {
198
- // Block silently: a reaction is a passive signal, so the message
199
- // pipeline's "storage is low, try again" notice is meaningless for an
200
- // emoji — there is nothing to retry. Mark the event processed and stop
201
- // before binding/persistence.
202
- if (!result.duplicate) {
203
- clearPayload(result.eventId);
204
- markProcessed(result.eventId);
205
- }
206
182
  return {
207
183
  accepted: true,
208
- duplicate: result.duplicate,
209
- eventId: result.eventId,
184
+ reaction: "dropped_disk_pressure",
210
185
  diskPressure: "blocked",
211
186
  reason: diskPressure.reason,
212
187
  };
213
188
  }
214
189
 
215
- // Maintain the conversation binding, matching the message pipeline.
216
- upsertBinding({
217
- conversationId: result.conversationId,
190
+ // A redelivery is answered before anything acts on it. Recording the event
191
+ // is what dedups a reaction, and that happens after the guardian rail, so
192
+ // without this probe the same emoji could drive a decision twice.
193
+ const alreadyRecorded = findInboundEvent(
218
194
  sourceChannel,
219
- externalChatId: conversationExternalId,
220
- externalChatName: slackChannelName,
221
- externalThreadId: threadTs ?? null,
222
- externalUserId: canonicalSenderId ?? rawSenderId ?? null,
223
- displayName: actorDisplayName ?? null,
224
- username: actorUsername ?? null,
225
- });
195
+ conversationExternalId,
196
+ externalMessageId,
197
+ );
198
+ if (alreadyRecorded) {
199
+ return {
200
+ accepted: true,
201
+ duplicate: true,
202
+ eventId: alreadyRecorded.eventId,
203
+ };
204
+ }
226
205
 
227
- // Guardian approval-by-reaction → guardian decision pipeline, exactly like
228
- // buttons and text replies. Only `reaction:` (added) expresses intent;
206
+ // Guardian approval-by-reaction runs before the reacted message is resolved:
207
+ // an approval card is addressed by its own message id and is not always a
208
+ // stored message. Only `reaction:` (added) expresses intent;
229
209
  // `reaction_removed:` never does. `handleGuardianReplyIntercept` self-gates
230
- // on `trustClass === "guardian"`, so a contact's reaction returns no response
231
- // and falls through to persistence.
232
- const isReactionAdded = callbackData.startsWith("reaction:");
233
- if (isReactionAdded && replyCallbackUrl && !result.duplicate) {
210
+ // on `trustClass === "guardian"`, so a contact's reaction returns no
211
+ // response and falls through to persistence.
212
+ if (callbackData.startsWith("reaction:") && replyCallbackUrl) {
234
213
  const reactionIntercept = await handleGuardianReplyIntercept({
235
- isDuplicate: result.duplicate,
214
+ isDuplicate: false,
236
215
  trimmedContent: "",
237
216
  hasCallbackData: true,
238
217
  callbackData,
@@ -241,35 +220,56 @@ export async function handleSlackReactionIntercept(
241
220
  canonicalSenderId,
242
221
  sourceChannel,
243
222
  conversationExternalId,
244
- conversationId: result.conversationId,
245
- eventId: result.eventId,
246
223
  replyCallbackUrl,
247
224
  trustClass: trustCtx.trustClass,
248
225
  guardianPrincipalId: trustCtx.guardianPrincipalId,
249
226
  approvalConversationGenerator,
250
227
  });
251
- // Consumed as a guardian decision (applied, or a surfaced failure delivered
252
- // as an ephemeral reply). Short-circuit so we do not also persist a
253
- // transcript row.
254
228
  if (reactionIntercept.response) {
255
229
  return reactionIntercept.response;
256
230
  }
257
231
  }
258
232
 
259
- // Record the reaction as an inline transcript signal. Requires the reacted
260
- // message ts to anchor the rendering.
261
- if (!reactedMessageTs) {
233
+ // The reaction belongs to the conversation of the message it was attached
234
+ // to. Slack sends no `thread_ts` on a reaction, so resolving a conversation
235
+ // from the reaction's own address keys one on the reacted message instead of
236
+ // finding the one that message lives in, minting an orphan per reaction.
237
+ // A message the assistant never stored has nothing to annotate, so the
238
+ // reaction is dropped rather than given a conversation of its own.
239
+ // Inbound messages carry their provider id on the event that delivered
240
+ // them. The assistant's own posts open no inbound event, so a reaction on
241
+ // one is resolved through the `slackMeta` those rows carry.
242
+ const targetConversationId = reactedMessageTs
243
+ ? (findMessageBySourceId(
244
+ sourceChannel,
245
+ conversationExternalId,
246
+ reactedMessageTs,
247
+ )?.conversationId ??
248
+ findSlackConversationByMessageTs(
249
+ conversationExternalId,
250
+ reactedMessageTs,
251
+ ))
252
+ : null;
253
+ if (!targetConversationId || !reactedMessageTs) {
262
254
  log.debug(
263
- { conversationId: result.conversationId, eventId: result.eventId },
264
- "Skipping reaction persistence: missing sourceMetadata.messageId",
255
+ { sourceChannel, conversationExternalId, reactedMessageTs },
256
+ "Dropping reaction: reacted message is not stored",
265
257
  );
266
- return {
267
- accepted: result.accepted,
268
- duplicate: result.duplicate,
269
- eventId: result.eventId,
270
- };
258
+ return { accepted: true, reaction: "dropped_unknown_target" };
271
259
  }
272
260
 
261
+ const result = recordInbound(
262
+ sourceChannel,
263
+ conversationExternalId,
264
+ externalMessageId,
265
+ // No `sourceMessageId`: that column names the provider id of the event's
266
+ // own message, and a reaction is not one. Claiming the reacted message's
267
+ // id here would put two linked rows on that id, leaving a later edit or
268
+ // delete of it free to resolve to the reaction instead.
269
+ { conversationId: targetConversationId },
270
+ );
271
+
272
+ // Record the reaction as an inline transcript signal.
273
273
  try {
274
274
  await persistSlackReactionAsMessage({
275
275
  conversationId: result.conversationId,
@@ -277,7 +277,6 @@ export async function handleSlackReactionIntercept(
277
277
  eventId: result.eventId,
278
278
  callbackData,
279
279
  actorDisplayName,
280
- threadTs,
281
280
  reactedMessageTs,
282
281
  duplicate: result.duplicate,
283
282
  });
@@ -311,7 +310,6 @@ async function persistSlackReactionAsMessage(params: {
311
310
  eventId: string;
312
311
  callbackData: string;
313
312
  actorDisplayName?: string;
314
- threadTs?: string;
315
313
  reactedMessageTs: string;
316
314
  duplicate: boolean;
317
315
  }): Promise<void> {
@@ -336,7 +334,11 @@ async function persistSlackReactionAsMessage(params: {
336
334
  channelId: params.conversationExternalId,
337
335
  channelTs: params.reactedMessageTs,
338
336
  eventKind: "reaction",
339
- ...(params.threadTs ? { threadTs: params.threadTs } : {}),
337
+ // No `threadTs`: Slack sends none on a reaction, so the gateway's thread
338
+ // id here is the reacted message's own ts. It equals `channelTs`, which
339
+ // every reader treats as "not in a thread" anyway, and storing it makes
340
+ // the row false evidence that a thread belongs to this conversation
341
+ // (`legacySlackConversationHasThreadEvidence`).
340
342
  ...(params.actorDisplayName
341
343
  ? { displayName: params.actorDisplayName }
342
344
  : {}),
@@ -127,6 +127,7 @@ import { ROUTES as PUBLISH_ROUTES } from "./publish-routes.js";
127
127
  import { ROUTES as QUESTION_ROUTES } from "./question-routes.js";
128
128
  import { ROUTES as RECORDING_ROUTES } from "./recording-routes.js";
129
129
  import { ROUTES as RENAME_CONVERSATION_ROUTES } from "./rename-conversation-routes.js";
130
+ import { ROUTES as RESOURCE_PRESSURE_ROUTES } from "./resource-pressure-routes.js";
130
131
  import { ROUTES as RETROSPECTIVE_ROUTES } from "./retrospective-routes.js";
131
132
  import { ROUTES as ROUTE_HOST_WORKER_ROUTES } from "./route-host-worker-routes.js";
132
133
  import { ROUTES as SANITY_ROUTES } from "./sanity-routes.js";
@@ -210,6 +211,7 @@ export const ROUTES: RouteDefinition[] = [
210
211
  ...DEFAULT_PROVIDER_ROUTES,
211
212
  ...DIAGNOSTICS_ROUTES,
212
213
  ...DISK_PRESSURE_ROUTES,
214
+ ...RESOURCE_PRESSURE_ROUTES,
213
215
  ...DOMAIN_ROUTES,
214
216
  ...DOCUMENT_COMMENT_ROUTES,
215
217
  ...DOCUMENT_ROUTES,
@@ -27,7 +27,7 @@ import {
27
27
  ConnectionProviderSchema,
28
28
  deriveAuthForProvider,
29
29
  ProviderConnectionSchema,
30
- PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
30
+ PROVIDERS_ALLOWING_CUSTOM_BASE_URL,
31
31
  VALID_CONNECTION_PROVIDERS,
32
32
  } from "../../providers/inference/auth.js";
33
33
  import {
@@ -72,14 +72,16 @@ const providerConnectionResponseSchema = ProviderConnectionSchema;
72
72
  * Parse and validate `base_url` and `models` from the request body.
73
73
  *
74
74
  * `base_url` is only accepted for providers in
75
- * `PROVIDERS_REQUIRING_BASE_URL_AND_MODELS` (currently `openai-compatible`).
75
+ * `PROVIDERS_ALLOWING_CUSTOM_BASE_URL` (openai-compatible and ollama).
76
76
  * For all other providers, supplying `base_url` returns a 400. This prevents
77
77
  * API-key exfiltration: an attacker cannot create an `anthropic` connection
78
78
  * with a `base_url` pointing to their own server, which would redirect all
79
79
  * LLM calls (and the API key) to the attacker.
80
80
  *
81
- * Even for `openai-compatible`, the `base_url` must not point to private
82
- * networks or cloud metadata endpoints (SSRF protection).
81
+ * Even for allowed providers, a platform-hosted daemon rejects `base_url`
82
+ * values that point to private networks or cloud metadata endpoints
83
+ * (SSRF protection). Self-hosted daemons allow those addresses because
84
+ * localhost and LAN hosts are the expected target.
83
85
  */
84
86
  async function parseCustomProviderFields(
85
87
  body: Record<string, unknown>,
@@ -96,14 +98,14 @@ async function parseCustomProviderFields(
96
98
  if ("base_url" in body) {
97
99
  const raw = body.base_url;
98
100
 
99
- // Gate: base_url is only valid for openai-compatible providers.
101
+ // Gate: base_url is only valid for providers that persist a custom endpoint.
100
102
  if (
101
103
  raw !== null &&
102
104
  raw !== undefined &&
103
- !PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(provider)
105
+ !PROVIDERS_ALLOWING_CUSTOM_BASE_URL.has(provider)
104
106
  ) {
105
107
  throw new BadRequestError(
106
- `base_url is only valid for openai-compatible providers. Remove base_url or use the openai-compatible provider type.`,
108
+ `base_url is only valid for openai-compatible and ollama providers. Remove base_url or use a provider that accepts a custom endpoint.`,
107
109
  );
108
110
  }
109
111
 
@@ -56,6 +56,7 @@ const sampleEventsSchema = z.object({
56
56
  const sampleMemoryStatSchema = z.object({
57
57
  anonBytes: z.number().nullable(),
58
58
  fileBytes: z.number().nullable(),
59
+ inactiveFileBytes: z.number().nullable(),
59
60
  kernelBytes: z.number().nullable(),
60
61
  slabReclaimableBytes: z.number().nullable(),
61
62
  slabUnreclaimableBytes: z.number().nullable(),
@@ -195,9 +196,16 @@ function readLatestSample(): ResourceSample | null {
195
196
  }
196
197
  // Samples persisted by an older monitor may predate some fields; fill
197
198
  // them with null so the response always matches the documented shape.
199
+ // The nested memoryStat needs the same backfill for fields added after
200
+ // a sample was written.
198
201
  return {
199
202
  ...sample,
200
- memoryStat: sample.memoryStat ?? null,
203
+ memoryStat: sample.memoryStat
204
+ ? {
205
+ ...sample.memoryStat,
206
+ inactiveFileBytes: sample.memoryStat.inactiveFileBytes ?? null,
207
+ }
208
+ : null,
201
209
  reclaim: sample.reclaim ?? null,
202
210
  cpu: sample.cpu ?? null,
203
211
  deltas: sample.deltas ?? null,
@@ -1643,7 +1643,7 @@ function mapTogglePluginError(err: unknown): RouteError {
1643
1643
 
1644
1644
  /**
1645
1645
  * Converge plugin-declared schedules against the sentinel this route just
1646
- * wrote, so a toggled plugin's rows disarm or re-arm now rather than at the
1646
+ * wrote, so a disabled plugin's rows disarm now rather than at the
1647
1647
  * reconciler's next backstop sweep.
1648
1648
  *
1649
1649
  * Fire-and-forget: the toggle itself already succeeded on disk, so a reconcile
@@ -1671,15 +1671,27 @@ function reconcilePluginSchedulesInBackground(): void {
1671
1671
  * names WHICH resource is stale, not the new value. The origin client id is
1672
1672
  * threaded through for self-echo suppression.
1673
1673
  */
1674
- function handleEnablePlugin({ pathParams = {}, headers }: RouteHandlerArgs) {
1674
+ async function handleEnablePlugin({
1675
+ pathParams = {},
1676
+ headers,
1677
+ }: RouteHandlerArgs) {
1675
1678
  try {
1676
1679
  enablePlugin(pathParams.name ?? "");
1677
- publishPluginsChanged(getOriginClientId(headers));
1678
- reconcilePluginSchedulesInBackground();
1679
- return { ok: true };
1680
1680
  } catch (err) {
1681
1681
  throw mapTogglePluginError(err);
1682
1682
  }
1683
+ // A plugin the boot scan skipped for its sentinel has no hooks, tools or
1684
+ // MCP servers in the caches, so clearing the sentinel is not enough to
1685
+ // bring it up. Run the same imperative source reconcile the install and
1686
+ // upgrade routes use, which activates the plugin and then converges its
1687
+ // schedules and MCP servers. Awaited, like the install route, so a client
1688
+ // that lists tools right after enabling sees the plugin up; the reconcile
1689
+ // contains its own failures, so a broken plugin cannot turn a successful
1690
+ // enable into a route error. The invalidation publishes after it for the
1691
+ // same reason: refetching clients should see the post-activation state.
1692
+ await reconcilePluginSourcesNow();
1693
+ publishPluginsChanged(getOriginClientId(headers));
1694
+ return { ok: true };
1683
1695
  }
1684
1696
 
1685
1697
  function handleDisablePlugin({ pathParams = {}, headers }: RouteHandlerArgs) {
@@ -0,0 +1,22 @@
1
+ import { ResourcePressureStatusResponseSchema } from "../../api/responses/resource-pressure-status.js";
2
+ import { getResourcePressureStatus } from "../../daemon/resource-pressure-guard.js";
3
+ import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
4
+ import type { RouteDefinition } from "./types.js";
5
+
6
+ export const ROUTES: RouteDefinition[] = [
7
+ {
8
+ operationId: "getResourcePressureStatus",
9
+ endpoint: "resource-pressure/status",
10
+ method: "GET",
11
+ policy: {
12
+ requiredScopes: ["settings.read"],
13
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
14
+ },
15
+ summary: "Get resource pressure status",
16
+ description:
17
+ "Return the current resource pressure status snapshot. Off-platform the guard does not run, so a disabled status is returned.",
18
+ tags: ["resource-pressure"],
19
+ responseBody: ResourcePressureStatusResponseSchema,
20
+ handler: () => ({ status: getResourcePressureStatus() }),
21
+ },
22
+ ];
@@ -58,7 +58,16 @@ import {
58
58
  } from "../../tools/credentials/metadata-store.js";
59
59
  import { getLogger } from "../../util/logger.js";
60
60
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
61
- import { BadRequestError, InternalError, NotFoundError } from "./errors.js";
61
+ import {
62
+ assertCredentialNotInUse,
63
+ invalidateConnectionsAfterCredentialDelete,
64
+ } from "./credential-in-use.js";
65
+ import {
66
+ BadRequestError,
67
+ InternalError,
68
+ NotFoundError,
69
+ RouteError,
70
+ } from "./errors.js";
62
71
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
63
72
 
64
73
  const log = getLogger("runtime-http");
@@ -550,7 +559,11 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
550
559
  throw new BadRequestError("Request body is required");
551
560
  }
552
561
 
553
- const { type, name } = body as { type?: string; name?: string };
562
+ const { type, name, force } = body as {
563
+ type?: string;
564
+ name?: string;
565
+ force?: boolean;
566
+ };
554
567
 
555
568
  if (!type || typeof type !== "string") {
556
569
  throw new BadRequestError("type is required");
@@ -583,6 +596,13 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
583
596
  throw new NotFoundError(`API key not found: ${name}`);
584
597
  }
585
598
  }
599
+ // Provider connections reference the namespaced account, so the in-use
600
+ // check keys off `credKey`: the bare pre-migration account is never a
601
+ // connection's `resolve_auth` target.
602
+ const affectedConnections = assertCredentialNotInUse(
603
+ credKey,
604
+ force === true,
605
+ );
586
606
  // Delete from both locations. During a migration overlap both may exist;
587
607
  // ignore "not-found" since one location may already be empty.
588
608
  const credDeleteResult = await deleteSecureKeyAsync(credKey);
@@ -598,6 +618,7 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
598
618
  );
599
619
  }
600
620
  await refreshProvidersAfterSecretChange();
621
+ invalidateConnectionsAfterCredentialDelete(affectedConnections);
601
622
  log.info({ provider: name }, "API key deleted via HTTP");
602
623
  return { success: true, type, name };
603
624
  }
@@ -617,6 +638,7 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
617
638
  if (existing === undefined) {
618
639
  throw new NotFoundError(`Credential not found: ${name}`);
619
640
  }
641
+ const affectedConnections = assertCredentialNotInUse(key, force === true);
620
642
  const deleteResult = await deleteSecureKeyAsync(key);
621
643
  if (deleteResult === "error") {
622
644
  throw new InternalError(
@@ -639,6 +661,7 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
639
661
  if (isManagedProxyCredential(service, field)) {
640
662
  await refreshProvidersAfterSecretChange();
641
663
  }
664
+ invalidateConnectionsAfterCredentialDelete(affectedConnections);
642
665
  log.info({ service, field }, "Credential deleted via HTTP");
643
666
  return { success: true, type, name };
644
667
  }
@@ -647,11 +670,10 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
647
670
  `Unknown secret type: ${type}. Valid types: api_key, credential`,
648
671
  );
649
672
  } catch (err) {
650
- if (
651
- err instanceof BadRequestError ||
652
- err instanceof InternalError ||
653
- err instanceof NotFoundError
654
- ) {
673
+ // Every RouteError is already a deliberate client-facing outcome (including
674
+ // CREDENTIAL_IN_USE); re-wrapping one as a 500 would hide its code, status,
675
+ // and details from the caller.
676
+ if (err instanceof RouteError) {
655
677
  throw err;
656
678
  }
657
679
  const message = err instanceof Error ? err.message : String(err);
@@ -793,11 +815,20 @@ export const ROUTES: RouteDefinition[] = [
793
815
  allowedPrincipalTypes: ACTOR_PRINCIPALS,
794
816
  },
795
817
  summary: "Delete a secret",
796
- description: "Remove a secret from the credential vault by name.",
818
+ description:
819
+ "Remove a secret from the credential vault by name. Refused with " +
820
+ "CREDENTIAL_IN_USE while an LLM provider connection resolves its auth " +
821
+ "through the credential, unless `force` is set.",
797
822
  tags: ["secrets"],
798
823
  requestBody: z.object({
799
824
  type: z.string().describe("Secret type: 'api_key' or 'credential'"),
800
825
  name: z.string().describe("Name of the secret to delete"),
826
+ force: z
827
+ .boolean()
828
+ .optional()
829
+ .describe(
830
+ "Delete even when provider connections depend on the credential",
831
+ ),
801
832
  }),
802
833
  responseBody: z.object({
803
834
  success: z.boolean(),