@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
@@ -0,0 +1,428 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { ResourcePressureStatusChangedEventSchema } from "../api/events/resource-pressure-status-changed.js";
4
+ import type { ResourcePressureStatus } from "../daemon/resource-pressure-guard.js";
5
+
6
+ const broadcasts: unknown[] = [];
7
+
8
+ mock.module("../runtime/assistant-event-hub.js", () => ({
9
+ AssistantEventHub: class {},
10
+ broadcastMessage: (message: unknown) => {
11
+ broadcasts.push(message);
12
+ },
13
+ capabilityForMessageType: () => undefined,
14
+ assistantEventHub: {
15
+ publish: async () => {},
16
+ },
17
+ }));
18
+
19
+ // Default readers report no container accounting, so tests that exercise the
20
+ // default samplers see both signals unavailable. Individual tests override
21
+ // the mutable values to exercise other default-sampler paths.
22
+ let mockedCpuCores = 0;
23
+ let mockedCachedCpuPercentOrNull: number | null = null;
24
+
25
+ mock.module("../util/container-cpu-sampler.js", () => ({
26
+ getCachedContainerCpuPercent: () => mockedCachedCpuPercentOrNull ?? 0,
27
+ getCachedContainerCpuPercentOrNull: () => mockedCachedCpuPercentOrNull,
28
+ getAverageContainerCpuPercentOrNull: (_windowMs: number) =>
29
+ mockedCachedCpuPercentOrNull,
30
+ }));
31
+
32
+ mock.module("../util/cgroup-cpu.js", () => ({
33
+ getContainerCpuCores: () => mockedCpuCores,
34
+ }));
35
+
36
+ let mockedMemoryLimitBytes: number | null = null;
37
+ let mockedMemoryUsageBytes: number | null = null;
38
+ let mockedMemoryStat: {
39
+ inactiveFileBytes: number | null;
40
+ slabReclaimableBytes: number | null;
41
+ } | null = null;
42
+
43
+ mock.module("../util/cgroup-memory.js", () => ({
44
+ getContainerMemoryLimitBytes: () => mockedMemoryLimitBytes,
45
+ getContainerMemoryUsageBytes: () => mockedMemoryUsageBytes,
46
+ getContainerMemoryStat: () => mockedMemoryStat,
47
+ }));
48
+
49
+ const {
50
+ RESOURCE_PRESSURE_CLEAR_CONSECUTIVE_SAMPLES,
51
+ RESOURCE_PRESSURE_ENTER_SAMPLES,
52
+ RESOURCE_PRESSURE_WINDOW_SAMPLES,
53
+ __getResourcePressureGuardTimerForTests,
54
+ __resetResourcePressureGuardForTests,
55
+ evaluateResourcePressureNow,
56
+ getResourcePressureStatus,
57
+ startResourcePressureGuard,
58
+ stopResourcePressureGuard,
59
+ } = await import("../daemon/resource-pressure-guard.js");
60
+
61
+ function evaluateCpu(percent: number): ResourcePressureStatus {
62
+ return evaluateResourcePressureNow({
63
+ sampleCpuPercent: () => percent,
64
+ sampleMemory: () => null,
65
+ });
66
+ }
67
+
68
+ function driveCpu(count: number, percent: number): ResourcePressureStatus {
69
+ let status: ResourcePressureStatus | null = null;
70
+ for (let i = 0; i < count; i += 1) {
71
+ status = evaluateCpu(percent);
72
+ }
73
+ return status!;
74
+ }
75
+
76
+ function driveMemory(
77
+ count: number,
78
+ sample: { usageBytes: number; limitBytes: number; reclaimableBytes: number },
79
+ ): ResourcePressureStatus {
80
+ let status: ResourcePressureStatus | null = null;
81
+ for (let i = 0; i < count; i += 1) {
82
+ status = evaluateResourcePressureNow({
83
+ sampleCpuPercent: () => null,
84
+ sampleMemory: () => sample,
85
+ });
86
+ }
87
+ return status!;
88
+ }
89
+
90
+ function enterCpuElevated(): ResourcePressureStatus {
91
+ const idleSamples =
92
+ RESOURCE_PRESSURE_WINDOW_SAMPLES - RESOURCE_PRESSURE_ENTER_SAMPLES;
93
+ driveCpu(idleSamples, 10);
94
+ const status = driveCpu(RESOURCE_PRESSURE_ENTER_SAMPLES, 95);
95
+ expect(status.state).toBe("elevated");
96
+ return status;
97
+ }
98
+
99
+ const originalIsPlatform = process.env.IS_PLATFORM;
100
+
101
+ beforeEach(() => {
102
+ __resetResourcePressureGuardForTests();
103
+ broadcasts.length = 0;
104
+ mockedCpuCores = 0;
105
+ mockedCachedCpuPercentOrNull = null;
106
+ mockedMemoryLimitBytes = null;
107
+ mockedMemoryUsageBytes = null;
108
+ mockedMemoryStat = null;
109
+ process.env.IS_PLATFORM = "true";
110
+ });
111
+
112
+ afterEach(() => {
113
+ __resetResourcePressureGuardForTests();
114
+ broadcasts.length = 0;
115
+ if (originalIsPlatform === undefined) {
116
+ delete process.env.IS_PLATFORM;
117
+ } else {
118
+ process.env.IS_PLATFORM = originalIsPlatform;
119
+ }
120
+ });
121
+
122
+ describe("resource pressure guard", () => {
123
+ test("stays disabled off-platform: no timer, no samples, no broadcasts", () => {
124
+ delete process.env.IS_PLATFORM;
125
+
126
+ const started = startResourcePressureGuard();
127
+
128
+ expect(started.enabled).toBe(false);
129
+ expect(started.state).toBe("disabled");
130
+ expect(__getResourcePressureGuardTimerForTests()).toBeNull();
131
+
132
+ const evaluated = evaluateCpu(99);
133
+ expect(evaluated.enabled).toBe(false);
134
+ expect(evaluated.state).toBe("disabled");
135
+
136
+ const status = getResourcePressureStatus();
137
+ expect(status.enabled).toBe(false);
138
+ expect(status.state).toBe("disabled");
139
+
140
+ expect(broadcasts.length).toBe(0);
141
+ });
142
+
143
+ test("17 of 20 samples over the CPU threshold stays ok", () => {
144
+ driveCpu(3, 10);
145
+ const status = driveCpu(17, 95);
146
+
147
+ expect(status.state).toBe("ok");
148
+ expect(status.cpuElevated).toBe(false);
149
+ expect(status.cpuPercent).toBe(95);
150
+ });
151
+
152
+ test("18 of 20 samples over the CPU threshold enters elevated", () => {
153
+ const status = enterCpuElevated();
154
+
155
+ expect(status.cpuElevated).toBe(true);
156
+ expect(status.memoryElevated).toBe(false);
157
+ expect(status.enabled).toBe(true);
158
+ });
159
+
160
+ test("does not enter elevated before the window is full", () => {
161
+ let status = evaluateCpu(95);
162
+ for (let i = 1; i < RESOURCE_PRESSURE_WINDOW_SAMPLES - 1; i += 1) {
163
+ status = evaluateCpu(95);
164
+ expect(status.state).toBe("ok");
165
+ }
166
+
167
+ status = evaluateCpu(95);
168
+ expect(status.state).toBe("elevated");
169
+ });
170
+
171
+ test("a 10-sample spike inside an otherwise idle window never enters elevated", () => {
172
+ const statuses: ResourcePressureStatus[] = [];
173
+ for (let i = 0; i < 5; i += 1) {
174
+ statuses.push(evaluateCpu(10));
175
+ }
176
+ for (let i = 0; i < 10; i += 1) {
177
+ statuses.push(evaluateCpu(95));
178
+ }
179
+ for (let i = 0; i < 10; i += 1) {
180
+ statuses.push(evaluateCpu(10));
181
+ }
182
+
183
+ for (const status of statuses) {
184
+ expect(status.state).toBe("ok");
185
+ expect(status.cpuElevated).toBe(false);
186
+ }
187
+ });
188
+
189
+ test("holds elevated between the clear and enter thresholds, clears after 10 consecutive below clear", () => {
190
+ enterCpuElevated();
191
+
192
+ // 75% is below the 85% enter threshold but above the 70% clear threshold:
193
+ // hysteresis holds the elevated state.
194
+ for (let i = 0; i < 15; i += 1) {
195
+ expect(evaluateCpu(75).state).toBe("elevated");
196
+ }
197
+
198
+ // Below the clear threshold, but not yet for long enough.
199
+ let status = driveCpu(RESOURCE_PRESSURE_CLEAR_CONSECUTIVE_SAMPLES - 1, 60);
200
+ expect(status.state).toBe("elevated");
201
+
202
+ // The 10th consecutive sample below the clear threshold clears it.
203
+ status = evaluateCpu(60);
204
+ expect(status.state).toBe("ok");
205
+ expect(status.cpuElevated).toBe(false);
206
+ });
207
+
208
+ test("a dip above the clear threshold resets the consecutive clear count", () => {
209
+ enterCpuElevated();
210
+
211
+ for (
212
+ let i = 0;
213
+ i < RESOURCE_PRESSURE_CLEAR_CONSECUTIVE_SAMPLES - 1;
214
+ i += 1
215
+ ) {
216
+ expect(evaluateCpu(60).state).toBe("elevated");
217
+ }
218
+ // Back above clear: the run restarts.
219
+ expect(evaluateCpu(75).state).toBe("elevated");
220
+ for (
221
+ let i = 0;
222
+ i < RESOURCE_PRESSURE_CLEAR_CONSECUTIVE_SAMPLES - 1;
223
+ i += 1
224
+ ) {
225
+ expect(evaluateCpu(60).state).toBe("elevated");
226
+ }
227
+ expect(evaluateCpu(60).state).toBe("ok");
228
+ });
229
+
230
+ test("reclaimable file cache is subtracted from the memory working set", () => {
231
+ // Usage at 95% of the limit, but 20% of the limit is reclaimable page
232
+ // cache: the working set is 75%, under the 90% threshold.
233
+ const status = driveMemory(RESOURCE_PRESSURE_WINDOW_SAMPLES + 5, {
234
+ usageBytes: 950,
235
+ limitBytes: 1000,
236
+ reclaimableBytes: 200,
237
+ });
238
+
239
+ expect(status.state).toBe("ok");
240
+ expect(status.memoryElevated).toBe(false);
241
+ expect(status.memoryPercent).toBe(75);
242
+ });
243
+
244
+ test("unreclaimable memory over the threshold enters elevated", () => {
245
+ const status = driveMemory(RESOURCE_PRESSURE_WINDOW_SAMPLES, {
246
+ usageBytes: 950,
247
+ limitBytes: 1000,
248
+ reclaimableBytes: 0,
249
+ });
250
+
251
+ expect(status.state).toBe("elevated");
252
+ expect(status.memoryElevated).toBe(true);
253
+ expect(status.cpuElevated).toBe(false);
254
+ expect(status.memoryPercent).toBe(95);
255
+ });
256
+
257
+ test("broadcasts exactly on ok to elevated and elevated to ok transitions", () => {
258
+ enterCpuElevated();
259
+ expect(broadcasts.length).toBe(1);
260
+
261
+ // More elevated samples do not re-broadcast.
262
+ driveCpu(5, 95);
263
+ expect(broadcasts.length).toBe(1);
264
+
265
+ driveCpu(RESOURCE_PRESSURE_CLEAR_CONSECUTIVE_SAMPLES, 60);
266
+ expect(broadcasts.length).toBe(2);
267
+
268
+ const elevatedEvent = ResourcePressureStatusChangedEventSchema.parse(
269
+ broadcasts[0],
270
+ );
271
+ expect(elevatedEvent.status.state).toBe("elevated");
272
+ expect(elevatedEvent.status.cpuElevated).toBe(true);
273
+
274
+ const clearedEvent = ResourcePressureStatusChangedEventSchema.parse(
275
+ broadcasts[1],
276
+ );
277
+ expect(clearedEvent.status.state).toBe("ok");
278
+ expect(clearedEvent.status.cpuElevated).toBe(false);
279
+
280
+ // Further ok samples stay silent.
281
+ driveCpu(5, 10);
282
+ expect(broadcasts.length).toBe(2);
283
+ });
284
+
285
+ test("missing memory limit and zero CPU cores yield unknown, never elevated", () => {
286
+ let status: ResourcePressureStatus | null = null;
287
+ for (let i = 0; i < RESOURCE_PRESSURE_WINDOW_SAMPLES + 5; i += 1) {
288
+ // No injected samplers: the mocked default readers report zero cores
289
+ // and a null memory limit.
290
+ status = evaluateResourcePressureNow();
291
+ expect(status.state).toBe("unknown");
292
+ expect(status.cpuElevated).toBe(false);
293
+ expect(status.memoryElevated).toBe(false);
294
+ }
295
+
296
+ expect(status!.enabled).toBe(true);
297
+ expect(status!.cpuPercent).toBeNull();
298
+ expect(status!.memoryPercent).toBeNull();
299
+ expect(status!.error).toBeTruthy();
300
+ expect(status!.lastCheckedAt).toBeTruthy();
301
+ });
302
+
303
+ test("the default memory sampler is unavailable without a reclaimable breakdown", () => {
304
+ // Usage and limit are readable but memory.stat is not (cgroups v1, or
305
+ // missing v2 counters): the working set is unknowable, so the signal
306
+ // must read unavailable instead of assuming zero reclaimable cache.
307
+ mockedMemoryLimitBytes = 1000;
308
+ mockedMemoryUsageBytes = 950;
309
+ mockedMemoryStat = null;
310
+
311
+ let status = evaluateResourcePressureNow();
312
+ expect(status.state).toBe("unknown");
313
+ expect(status.memoryPercent).toBeNull();
314
+
315
+ // An inactive_file counter alone is not enough to be missing: without
316
+ // it the readily-reclaimable split is unknowable even when slab counters
317
+ // parse.
318
+ mockedMemoryStat = { inactiveFileBytes: null, slabReclaimableBytes: 100 };
319
+ status = evaluateResourcePressureNow();
320
+ expect(status.memoryPercent).toBeNull();
321
+
322
+ // Only inactive file pages and reclaimable slab leave the working set;
323
+ // active file pages remain counted.
324
+ mockedMemoryStat = { inactiveFileBytes: 300, slabReclaimableBytes: 100 };
325
+ status = evaluateResourcePressureNow();
326
+ expect(status.state).toBe("ok");
327
+ expect(status.memoryPercent).toBe(55);
328
+ });
329
+
330
+ test("the default CPU sampler is unavailable until the rolling sampler warms up", () => {
331
+ // Cores are known but the rolling sampler has not computed a delta yet:
332
+ // the default sampler must report the signal unavailable rather than
333
+ // feed a genuine-looking 0% into the window.
334
+ mockedCpuCores = 4;
335
+
336
+ let status = evaluateResourcePressureNow();
337
+ expect(status.state).toBe("unknown");
338
+ expect(status.cpuPercent).toBeNull();
339
+ expect(status.cpuElevated).toBe(false);
340
+
341
+ // The first computed delta makes the signal available.
342
+ mockedCachedCpuPercentOrNull = 42;
343
+ status = evaluateResourcePressureNow();
344
+ expect(status.state).toBe("ok");
345
+ expect(status.cpuPercent).toBe(42);
346
+ });
347
+
348
+ test("an unavailable signal resets its window and drops its elevated flag", () => {
349
+ enterCpuElevated();
350
+
351
+ // CPU sample becomes unavailable: the signal cannot hold elevated.
352
+ const status = evaluateResourcePressureNow({
353
+ sampleCpuPercent: () => null,
354
+ sampleMemory: () => ({
355
+ usageBytes: 100,
356
+ limitBytes: 1000,
357
+ reclaimableBytes: 0,
358
+ }),
359
+ });
360
+
361
+ expect(status.state).toBe("ok");
362
+ expect(status.cpuElevated).toBe(false);
363
+ expect(status.cpuPercent).toBeNull();
364
+ expect(status.memoryPercent).toBe(10);
365
+ // A null sample is a legitimately unavailable signal, not an error.
366
+ expect(status.error).toBeNull();
367
+ });
368
+
369
+ test("a throwing sampler surfaces its error while the healthy signal still evaluates", () => {
370
+ const memorySample = {
371
+ usageBytes: 100,
372
+ limitBytes: 1000,
373
+ reclaimableBytes: 0,
374
+ };
375
+ const healthy = {
376
+ sampleCpuPercent: () => 10,
377
+ sampleMemory: () => memorySample,
378
+ };
379
+ const cpuThrows = {
380
+ sampleCpuPercent: () => {
381
+ throw new Error("cpu reader exploded");
382
+ },
383
+ sampleMemory: () => memorySample,
384
+ };
385
+
386
+ evaluateResourcePressureNow(healthy);
387
+ expect(broadcasts.length).toBe(0);
388
+
389
+ let status = evaluateResourcePressureNow(cpuThrows);
390
+ expect(status.state).toBe("ok");
391
+ expect(status.error).toContain("cpu reader exploded");
392
+ expect(status.cpuPercent).toBeNull();
393
+ expect(status.memoryPercent).toBe(10);
394
+ expect(broadcasts.length).toBe(1);
395
+
396
+ const failedEvent = ResourcePressureStatusChangedEventSchema.parse(
397
+ broadcasts[0],
398
+ );
399
+ expect(failedEvent.status.error).toContain("cpu reader exploded");
400
+
401
+ // Repeated failures do not re-broadcast.
402
+ status = evaluateResourcePressureNow(cpuThrows);
403
+ expect(status.error).toContain("cpu reader exploded");
404
+ expect(broadcasts.length).toBe(1);
405
+
406
+ // Recovery clears the error and broadcasts exactly once more.
407
+ status = evaluateResourcePressureNow(healthy);
408
+ expect(status.error).toBeNull();
409
+ expect(broadcasts.length).toBe(2);
410
+ });
411
+
412
+ test("timer start and stop are idempotent on platform", () => {
413
+ expect(__getResourcePressureGuardTimerForTests()).toBeNull();
414
+
415
+ startResourcePressureGuard();
416
+ const firstTimer = __getResourcePressureGuardTimerForTests();
417
+ expect(firstTimer).toBeTruthy();
418
+
419
+ startResourcePressureGuard();
420
+ expect(__getResourcePressureGuardTimerForTests()).toBe(firstTimer);
421
+
422
+ stopResourcePressureGuard();
423
+ expect(__getResourcePressureGuardTimerForTests()).toBeNull();
424
+
425
+ stopResourcePressureGuard();
426
+ expect(__getResourcePressureGuardTimerForTests()).toBeNull();
427
+ });
428
+ });
@@ -0,0 +1,113 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { ResourcePressureStatusResponseSchema } from "../api/responses/resource-pressure-status.js";
4
+
5
+ mock.module("../runtime/assistant-event-hub.js", () => ({
6
+ AssistantEventHub: class {},
7
+ broadcastMessage: () => {},
8
+ capabilityForMessageType: () => undefined,
9
+ assistantEventHub: {
10
+ publish: async () => {},
11
+ },
12
+ }));
13
+
14
+ mock.module("../util/container-cpu-sampler.js", () => ({
15
+ getCachedContainerCpuPercent: () => 0,
16
+ getCachedContainerCpuPercentOrNull: () => null,
17
+ getAverageContainerCpuPercentOrNull: (_windowMs: number) => null,
18
+ }));
19
+
20
+ mock.module("../util/cgroup-cpu.js", () => ({
21
+ getContainerCpuCores: () => 0,
22
+ }));
23
+
24
+ mock.module("../util/cgroup-memory.js", () => ({
25
+ getContainerMemoryLimitBytes: () => null,
26
+ getContainerMemoryUsageBytes: () => null,
27
+ getContainerMemoryStat: () => null,
28
+ }));
29
+
30
+ const {
31
+ __resetResourcePressureGuardForTests,
32
+ evaluateResourcePressureNow,
33
+ getResourcePressureStatus,
34
+ } = await import("../daemon/resource-pressure-guard.js");
35
+ const { ROUTES } =
36
+ await import("../runtime/routes/resource-pressure-routes.js");
37
+
38
+ const statusRoute = ROUTES.find(
39
+ (route) =>
40
+ route.endpoint === "resource-pressure/status" && route.method === "GET",
41
+ );
42
+
43
+ async function callStatusRoute() {
44
+ if (!statusRoute) {
45
+ throw new Error("GET resource-pressure/status route not registered");
46
+ }
47
+ return ResourcePressureStatusResponseSchema.parse(
48
+ await statusRoute.handler({}),
49
+ );
50
+ }
51
+
52
+ const originalIsPlatform = process.env.IS_PLATFORM;
53
+
54
+ function restoreIsPlatform(): void {
55
+ if (originalIsPlatform === undefined) {
56
+ delete process.env.IS_PLATFORM;
57
+ } else {
58
+ process.env.IS_PLATFORM = originalIsPlatform;
59
+ }
60
+ }
61
+
62
+ beforeEach(() => {
63
+ __resetResourcePressureGuardForTests();
64
+ });
65
+
66
+ afterEach(() => {
67
+ __resetResourcePressureGuardForTests();
68
+ restoreIsPlatform();
69
+ });
70
+
71
+ describe("resource pressure routes", () => {
72
+ test("registers the status route with a read-scoped auth policy", () => {
73
+ expect(statusRoute).toBeDefined();
74
+ expect(statusRoute?.policy?.requiredScopes).toEqual(["settings.read"]);
75
+ expect(statusRoute?.policy?.allowedPrincipalTypes?.length).toBeGreaterThan(
76
+ 0,
77
+ );
78
+ });
79
+
80
+ test("returns the disabled snapshot when the guard never started", async () => {
81
+ delete process.env.IS_PLATFORM;
82
+
83
+ const result = await callStatusRoute();
84
+
85
+ expect(result.status.enabled).toBe(false);
86
+ expect(result.status.state).toBe("disabled");
87
+ expect(result.status.cpuPercent).toBeNull();
88
+ expect(result.status.memoryPercent).toBeNull();
89
+ expect(result.status.lastCheckedAt).toBeNull();
90
+ expect(result.status.error).toBeNull();
91
+ });
92
+
93
+ test("returns the guard's current snapshot on platform", async () => {
94
+ process.env.IS_PLATFORM = "true";
95
+ evaluateResourcePressureNow({
96
+ sampleCpuPercent: () => 42,
97
+ sampleMemory: () => ({
98
+ usageBytes: 500,
99
+ limitBytes: 1000,
100
+ reclaimableBytes: 100,
101
+ }),
102
+ });
103
+
104
+ const result = await callStatusRoute();
105
+
106
+ expect(result.status).toEqual(getResourcePressureStatus());
107
+ expect(result.status.enabled).toBe(true);
108
+ expect(result.status.state).toBe("ok");
109
+ expect(result.status.cpuPercent).toBe(42);
110
+ expect(result.status.memoryPercent).toBe(40);
111
+ expect(result.status.lastCheckedAt).toBeTruthy();
112
+ });
113
+ });
@@ -0,0 +1,115 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ /**
5
+ * Guard test: risk classification is gateway-owned. The assistant is a client
6
+ * of the gateway's `classify_risk` method and nothing more (see
7
+ * `src/permissions/AGENTS.md` and `gateway/src/risk/AGENTS.md`).
8
+ *
9
+ * Two things this pins, because both have regrown before:
10
+ *
11
+ * 1. Exactly one production call site of `ipcClassifyRisk`, in
12
+ * `permissions/checker.ts` (`classifyRisk`). A second caller is a second
13
+ * classification per invocation, or a memo, in the making.
14
+ * 2. No production module under `assistant/src` classifies locally: no
15
+ * tree-sitter, no `RiskClassifier`, no risk registry. If a tool's risk is
16
+ * wrong, the fix is in `gateway/src/risk/`.
17
+ */
18
+
19
+ const ASSISTANT_SRC = "assistant/src";
20
+
21
+ /** The one production caller of the gateway classification method. */
22
+ const CLASSIFY_RISK_CALLER = "assistant/src/permissions/checker.ts";
23
+ /** Where `ipcClassifyRisk` is defined (a definition, not a call site). */
24
+ const CLASSIFY_RISK_CLIENT = "assistant/src/ipc/gateway-client.ts";
25
+
26
+ function isTestFile(path: string): boolean {
27
+ return (
28
+ path.includes("/__tests__/") ||
29
+ path.endsWith(".test.ts") ||
30
+ path.endsWith(".benchmark.test.ts")
31
+ );
32
+ }
33
+
34
+ /** `git grep -E` over assistant/src production TypeScript, one entry per match. */
35
+ function gitGrepProduction(pattern: string, ...flags: string[]): string[] {
36
+ const result = spawnSync(
37
+ "git",
38
+ ["grep", "-E", ...flags, pattern, "--", `${ASSISTANT_SRC}/*.ts`],
39
+ { encoding: "utf-8", cwd: process.cwd() + "/.." },
40
+ );
41
+ // Exit code 1 is "no matches"; anything else is a real failure.
42
+ if (result.status !== 0 && result.status !== 1) {
43
+ throw new Error(`git grep failed: ${result.stderr}`);
44
+ }
45
+ return result.stdout
46
+ .split("\n")
47
+ .filter((line) => line.length > 0)
48
+ .filter((line) => !isTestFile(line.split(":")[0]));
49
+ }
50
+
51
+ /** Files with at least one match. */
52
+ function productionFilesMatching(pattern: string): string[] {
53
+ return gitGrepProduction(pattern, "-l");
54
+ }
55
+
56
+ /** Every matching line as `file:line:text`. */
57
+ function productionLinesMatching(pattern: string): string[] {
58
+ return gitGrepProduction(pattern, "-n");
59
+ }
60
+
61
+ describe("risk classification boundary guard", () => {
62
+ test("the classify_risk method is spoken only by the IPC client", () => {
63
+ // A direct `"classify_risk"` call would bypass the client's contract
64
+ // validation and retry policy.
65
+ const files = productionFilesMatching('"classify_risk"');
66
+ expect(files, `Found: ${files.join(", ")}`).toEqual([CLASSIFY_RISK_CLIENT]);
67
+ });
68
+
69
+ test("ipcClassifyRisk has exactly one production call site", () => {
70
+ // Per line, not per file: a second call inside checker.ts is the case
71
+ // this exists to catch.
72
+ const calls = productionLinesMatching("ipcClassifyRisk\\(").filter(
73
+ (line) => !line.startsWith(`${CLASSIFY_RISK_CLIENT}:`),
74
+ );
75
+ expect(
76
+ calls,
77
+ [
78
+ "The gateway classifier must be called from exactly one place,",
79
+ `${CLASSIFY_RISK_CALLER} (classifyRisk). Calls found:`,
80
+ ...calls.map((c) => ` - ${c}`),
81
+ "",
82
+ "Thread the invocation's RiskClassificationWithMeta from classifyRisk",
83
+ "instead of classifying again.",
84
+ ].join("\n"),
85
+ ).toHaveLength(1);
86
+ expect(calls[0]).toStartWith(`${CLASSIFY_RISK_CALLER}:`);
87
+ expect(calls[0]).toContain("await ipcClassifyRisk(");
88
+ });
89
+
90
+ test("no production module under assistant/src classifies risk locally", () => {
91
+ const patterns = [
92
+ // Shell parsing belongs to the gateway.
93
+ "from ['\"](web-)?tree-sitter",
94
+ // A local classifier class or interface.
95
+ "(class|interface) [A-Za-z]*RiskClassifier\\b",
96
+ "implements RiskClassifier\\b",
97
+ // A local command risk registry.
98
+ "DEFAULT_COMMAND_REGISTRY\\b",
99
+ ];
100
+ const violations = patterns.flatMap((p) =>
101
+ productionFilesMatching(p).map((f) => `${f} (${p})`),
102
+ );
103
+ expect(
104
+ violations,
105
+ [
106
+ "Risk classification is gateway-owned; nothing under assistant/src may",
107
+ "classify locally. Found:",
108
+ ...violations.map((v) => ` - ${v}`),
109
+ "",
110
+ "Move the classifier into gateway/src/risk/ and read the answer from",
111
+ "classifyRisk().",
112
+ ].join("\n"),
113
+ ).toEqual([]);
114
+ });
115
+ });