@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
@@ -1,128 +1,125 @@
1
1
  /**
2
- * Tests for `prepareAgentEnv` — the shared helper that injects required env
2
+ * Tests for `prepareAgentEnv`, the shared helper that injects required env
3
3
  * vars onto an `AcpAgentConfig` and preflights that they're set.
4
4
  *
5
5
  * The route-level test in `runtime/routes/acp-routes.test.ts` covers the same
6
6
  * behavior through the HTTP handler; these tests pin the helper in isolation
7
7
  * so the contract is clear and a future refactor can't silently break it.
8
8
  *
9
- * Credential reads go through the credential broker (`serverUse`), so we
10
- * mock the broker and metadata store rather than the raw secure-keys backend.
9
+ * Credential reads go through the credential broker (`serverUse`), which is
10
+ * exercised for real here: the metadata store and the encrypted secure-key
11
+ * store are pointed at a temp dir, so the tool and domain policy these tests
12
+ * observe is the same policy production evaluates.
13
+ *
14
+ * The module logger is the one exception. `mock.module` does not hoist above
15
+ * static imports, so the module under test is imported dynamically AFTER the
16
+ * mock is installed: that makes `prepare-agent-env.js` the only module holding
17
+ * the spy logger, so every warn the spy records came from `prepareAgentEnv`.
11
18
  */
12
19
 
13
- import { beforeEach, describe, expect, mock, test } from "bun:test";
20
+ import { randomBytes } from "node:crypto";
21
+ import { mkdirSync, rmSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
14
25
 
26
+ import { setStorePathForTesting } from "../../__tests__/encrypted-store-test-helpers.js";
27
+ import { createMockLoggerModule } from "../../__tests__/helpers/mock-logger.js";
15
28
  import { FailedDependencyError } from "../../runtime/routes/errors.js";
16
-
17
- // ---------------------------------------------------------------------------
18
- // Stubs — wired BEFORE importing the helper via dynamic import.
19
- // ---------------------------------------------------------------------------
20
-
21
- /** Simulates the credential metadata store. */
22
- const metadataStore = new Map<
23
- string,
24
- { allowedTools: string[]; usageDescription?: string }
25
- >();
26
-
27
- mock.module("../../tools/credentials/metadata-store.js", () => ({
28
- getCredentialMetadata: (service: string, field: string) => {
29
- const key = `${service}/${field}`;
30
- const entry = metadataStore.get(key);
31
- if (!entry) {
32
- return undefined;
33
- }
34
- return {
35
- credentialId: `cred-${key}`,
36
- service,
37
- field,
38
- allowedTools: entry.allowedTools,
39
- allowedDomains: [],
40
- usageDescription: entry.usageDescription,
41
- createdAt: 0,
42
- updatedAt: 0,
43
- };
44
- },
45
- upsertCredentialMetadata: (
46
- service: string,
47
- field: string,
48
- policy?: { allowedTools?: string[]; usageDescription?: string },
49
- ) => {
50
- const key = `${service}/${field}`;
51
- const existing = metadataStore.get(key);
52
- metadataStore.set(key, {
53
- allowedTools: policy?.allowedTools ?? existing?.allowedTools ?? [],
54
- usageDescription: policy?.usageDescription ?? existing?.usageDescription,
55
- });
56
- return {
57
- credentialId: `cred-${key}`,
58
- service,
59
- field,
60
- allowedTools: metadataStore.get(key)!.allowedTools,
61
- allowedDomains: [],
62
- createdAt: 0,
63
- updatedAt: 0,
64
- };
65
- },
66
- }));
67
-
68
- /**
69
- * Simulates the credential broker's serverUse method. The vault stores
70
- * plaintext values keyed by `service/field`; the broker enforces tool
71
- * policy via the metadata store before passing the value to the callback.
72
- */
73
- const vaultStore = new Map<string, string>();
74
-
75
- mock.module("../../tools/credentials/broker.js", () => ({
76
- credentialBroker: {
77
- serverUse: async <T>(request: {
78
- service: string;
79
- field: string;
80
- toolName: string;
81
- execute: (value: string) => Promise<T>;
82
- }) => {
83
- const key = `${request.service}/${request.field}`;
84
- const meta = metadataStore.get(key);
85
- if (!meta) {
86
- return { success: false, reason: `No credential found for ${key}` };
87
- }
88
- if (!meta.allowedTools.includes(request.toolName)) {
89
- return {
90
- success: false,
91
- reason: `Tool "${request.toolName}" not allowed`,
92
- };
93
- }
94
- const value = vaultStore.get(key);
95
- if (!value) {
96
- return {
97
- success: false,
98
- reason: `No stored value for ${key}`,
99
- };
100
- }
101
- const result = await request.execute(value);
102
- return { success: true, result };
103
- },
104
- },
105
- }));
106
-
107
- const { prepareAgentEnv, ACP_CLAUDE_OAUTH_MISSING_CODE, grantAcpSpawnPolicy } =
108
- await import("../prepare-agent-env.js");
29
+ import { credentialKey } from "../../security/credential-key.js";
30
+ import {
31
+ _resetBackend,
32
+ setSecureKeyAsync,
33
+ } from "../../security/secure-keys.js";
34
+ import { credentialBroker } from "../../tools/credentials/broker.js";
35
+ import {
36
+ _setMetadataPath,
37
+ deleteCredentialMetadata,
38
+ getCredentialMetadata,
39
+ upsertCredentialMetadata,
40
+ } from "../../tools/credentials/metadata-store.js";
41
+ import { ACP_OAUTH_TOKEN_FIELD, ACP_SERVICE } from "../acp-credentials.js";
42
+
43
+ const mockLogWarn = mock((_fields: unknown, _msg: string) => {});
44
+
45
+ mock.module("../../util/logger.js", () =>
46
+ createMockLoggerModule({
47
+ getLogger: () => ({
48
+ warn: mockLogWarn,
49
+ info: () => {},
50
+ error: () => {},
51
+ debug: () => {},
52
+ trace: () => {},
53
+ fatal: () => {},
54
+ }),
55
+ }),
56
+ );
57
+
58
+ const {
59
+ ACP_CLAUDE_OAUTH_MISSING_CODE,
60
+ acpSpawnCredentialDenialReason,
61
+ ensureAcpCredentialPolicy,
62
+ prepareAgentEnv,
63
+ repairAcpSpawnPolicy,
64
+ } = await import("../prepare-agent-env.js");
65
+
66
+ const ACP_SPAWN_TOOL = "acp_spawn";
67
+
68
+ const TEST_DIR = join(
69
+ tmpdir(),
70
+ `vellum-prepare-agent-env-test-${randomBytes(4).toString("hex")}`,
71
+ );
109
72
 
110
73
  beforeEach(() => {
111
- metadataStore.clear();
112
- vaultStore.clear();
74
+ mkdirSync(TEST_DIR, { recursive: true });
75
+ setStorePathForTesting(join(TEST_DIR, "keys.enc"));
76
+ _resetBackend();
77
+ _setMetadataPath(join(TEST_DIR, "metadata.json"));
78
+ mockLogWarn.mockClear();
79
+ });
80
+
81
+ afterEach(() => {
82
+ _setMetadataPath(null);
83
+ setStorePathForTesting(null);
84
+ _resetBackend();
85
+ rmSync(TEST_DIR, { recursive: true, force: true });
113
86
  });
114
87
 
115
88
  // ---------------------------------------------------------------------------
116
- // Helper to seed a vault entry (simulates `assistant credentials set`).
89
+ // Helpers to seed the vault + metadata (simulates `assistant credentials set`).
117
90
  // ---------------------------------------------------------------------------
118
91
 
119
- function seedVaultToken(token: string): void {
120
- vaultStore.set("acp/claude_oauth_token", token);
92
+ async function seedVaultValue(field: string, value: string): Promise<void> {
93
+ await setSecureKeyAsync(credentialKey(ACP_SERVICE, field), value);
94
+ }
95
+
96
+ function seedVaultToken(token: string): Promise<void> {
97
+ return seedVaultValue(ACP_OAUTH_TOKEN_FIELD, token);
98
+ }
99
+
100
+ function oauthMetadata() {
101
+ return getCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD);
102
+ }
103
+
104
+ /**
105
+ * Structured fields of the single injection-miss warn the module emitted.
106
+ * Exactly one line per failed spawn: it is the operator's only record of WHY
107
+ * the token was missing, and a duplicate would double-count in log searches.
108
+ */
109
+ function soleInjectionMissWarnFields(): Record<string, unknown> {
110
+ expect(mockLogWarn).toHaveBeenCalledTimes(1);
111
+ const [fields, message] = mockLogWarn.mock.calls[0] as [
112
+ Record<string, unknown>,
113
+ string,
114
+ ];
115
+ expect(message).toBe("Claude OAuth token not injected for acp_spawn");
116
+ expect(fields.field).toBe(ACP_OAUTH_TOKEN_FIELD);
117
+ return fields;
121
118
  }
122
119
 
123
120
  describe("prepareAgentEnv — claude-agent-acp gating", () => {
124
121
  test("injects CLAUDE_CODE_OAUTH_TOKEN from the vault via the broker when agent.env has no override", async () => {
125
- seedVaultToken("vault-AAA");
122
+ await seedVaultToken("vault-AAA");
126
123
 
127
124
  const prepared = await prepareAgentEnv({
128
125
  command: "claude-agent-acp",
@@ -133,39 +130,35 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
133
130
  });
134
131
 
135
132
  test("auto-registers metadata with acp_spawn in allowedTools when none exists", async () => {
136
- seedVaultToken("vault-auto-meta");
133
+ await seedVaultToken("vault-auto-meta");
137
134
 
138
135
  await prepareAgentEnv({ command: "claude-agent-acp", args: [] });
139
136
 
140
- const meta = metadataStore.get("acp/claude_oauth_token");
141
- expect(meta).toBeDefined();
142
- expect(meta!.allowedTools).toContain("acp_spawn");
137
+ expect(oauthMetadata()?.allowedTools).toContain(ACP_SPAWN_TOOL);
143
138
  });
144
139
 
145
140
  test("adds acp_spawn to metadata with empty allowedTools (default provisioning path)", async () => {
146
- metadataStore.set("acp/claude_oauth_token", {
141
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
147
142
  allowedTools: [],
148
143
  });
149
- seedVaultToken("vault-augment");
144
+ await seedVaultToken("vault-augment");
150
145
 
151
146
  await prepareAgentEnv({ command: "claude-agent-acp", args: [] });
152
147
 
153
- const meta = metadataStore.get("acp/claude_oauth_token");
154
- expect(meta!.allowedTools).toContain("acp_spawn");
148
+ expect(oauthMetadata()?.allowedTools).toContain(ACP_SPAWN_TOOL);
155
149
  });
156
150
 
157
151
  test("respects explicit tool policy that excludes acp_spawn", async () => {
158
- metadataStore.set("acp/claude_oauth_token", {
152
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
159
153
  allowedTools: ["other_tool"],
160
154
  });
161
- seedVaultToken("vault-restricted");
155
+ await seedVaultToken("vault-restricted");
162
156
 
163
157
  await expect(
164
158
  prepareAgentEnv({ command: "claude-agent-acp", args: [] }),
165
159
  ).rejects.toThrow("CLAUDE_CODE_OAUTH_TOKEN");
166
160
 
167
- const meta = metadataStore.get("acp/claude_oauth_token");
168
- expect(meta!.allowedTools).toEqual(["other_tool"]);
161
+ expect(oauthMetadata()?.allowedTools).toEqual(["other_tool"]);
169
162
  });
170
163
 
171
164
  test("accepts CLAUDE_CODE_OAUTH_TOKEN from agent.env (config.json override) with no vault entry", async () => {
@@ -179,7 +172,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
179
172
  });
180
173
 
181
174
  test("agent.env override wins over the vault entry (precedence pin)", async () => {
182
- seedVaultToken("vault-CCC");
175
+ await seedVaultToken("vault-CCC");
183
176
 
184
177
  const prepared = await prepareAgentEnv({
185
178
  command: "claude-agent-acp",
@@ -191,7 +184,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
191
184
  });
192
185
 
193
186
  test("preserves unrelated env vars on agent.env when injecting from the vault", async () => {
194
- seedVaultToken("vault-EEE");
187
+ await seedVaultToken("vault-EEE");
195
188
 
196
189
  const prepared = await prepareAgentEnv({
197
190
  command: "claude-agent-acp",
@@ -244,10 +237,70 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
244
237
  // card actually renders above the model's reply, so "below" is always wrong.
245
238
  expect(message).toContain('never say "below"');
246
239
  expect(message).toContain('"above"');
240
+ // Pins the seam between the opening and the shared guidance: an absent
241
+ // value keeps the "which is not set" wording verbatim.
242
+ expect(message).toContain(
243
+ 'which is not set. The app shows the user an inline "Connect Claude Code" card.',
244
+ );
245
+
246
+ // The operator-facing half: the broker's own reason, classified.
247
+ const fields = soleInjectionMissWarnFields();
248
+ expect(fields.missReason).toContain("no stored value");
249
+ expect(fields.policyBlocked).toBe(false);
250
+ expect(fields.apiKeyShaped).toBe(false);
251
+ });
252
+
253
+ test("a policy-denied read logs the broker reason and reports the policy block", async () => {
254
+ // Invariant: a policy-denied read must be reported as a policy block, not
255
+ // as an absent value. The two states have different repair stories, and
256
+ // the message must never assert that a value is stored (policy is checked
257
+ // before the value, so metadata alone can trigger this branch).
258
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
259
+ allowedTools: ["bash"],
260
+ });
261
+ await seedVaultToken("sk-ant-oat01-policy-blocked");
262
+
263
+ let caught: unknown;
264
+ try {
265
+ await prepareAgentEnv({ command: "claude-agent-acp", args: [] });
266
+ } catch (err) {
267
+ caught = err;
268
+ }
269
+
270
+ expect(caught).toBeInstanceOf(FailedDependencyError);
271
+ // Same wire contract: old clients keep rendering the same Connect card.
272
+ expect((caught as FailedDependencyError).details).toEqual({
273
+ code: ACP_CLAUDE_OAUTH_MISSING_CODE,
274
+ });
275
+ const message = (caught as Error).message;
276
+ expect(message).toContain("cannot read the Claude OAuth token");
277
+ expect(message).toContain("blocks the acp_spawn read");
278
+ expect(message).toContain("CLAUDE_CODE_OAUTH_TOKEN");
279
+ expect(message).toContain("signs in again and repairs that policy");
280
+ // Policy is checked before the value, so the message must never assert
281
+ // that a value is actually stored.
282
+ expect(message).not.toContain("has a stored");
283
+ expect(message).not.toContain("which is not set");
284
+ // The guidance steering the model away from CLI/token-paste workarounds is
285
+ // shared with the missing-value variant, so it must survive here too.
286
+ expect(message).toContain("ONE short sentence");
287
+ expect(message).toContain("Do NOT");
288
+ expect(message).toContain("claude setup-token");
289
+ expect(message).toContain("do NOT retry the spawn yourself");
290
+ expect(message).toContain("assistant credentials prompt");
291
+
292
+ const fields = soleInjectionMissWarnFields();
293
+ expect(fields.missReason).toContain(
294
+ 'Tool "acp_spawn" is not allowed to use credential acp/claude_oauth_token',
295
+ );
296
+ expect(fields.policyBlocked).toBe(true);
297
+ expect(fields.apiKeyShaped).toBe(false);
298
+ // The denial reason is a policy verdict, never the credential itself.
299
+ expect(JSON.stringify(fields)).not.toContain("sk-ant-oat01-policy-blocked");
247
300
  });
248
301
 
249
302
  test("does NOT attach the marker when a token is present (happy path unchanged)", async () => {
250
- seedVaultToken("vault-marker-absent");
303
+ await seedVaultToken("vault-marker-absent");
251
304
 
252
305
  const prepared = await prepareAgentEnv({
253
306
  command: "claude-agent-acp",
@@ -262,7 +315,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
262
315
  // format guard existed must not spawn with a doomed credential (a 401 with
263
316
  // no repair). The presence check alone would pass, so the injected value is
264
317
  // classified and an API key is treated as missing → the Connect card fires.
265
- seedVaultToken("sk-ant-api03-legacy-bad-value");
318
+ await seedVaultToken("sk-ant-api03-legacy-bad-value");
266
319
 
267
320
  let caught: unknown;
268
321
  try {
@@ -275,6 +328,20 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
275
328
  expect((caught as FailedDependencyError).details).toEqual({
276
329
  code: ACP_CLAUDE_OAUTH_MISSING_CODE,
277
330
  });
331
+ // Connect genuinely repairs this by storing a fresh OAuth token, so the
332
+ // message stays the missing-token one rather than the policy-blocked one.
333
+ expect((caught as Error).message).toContain(
334
+ 'which is not set. The app shows the user an inline "Connect Claude Code" card.',
335
+ );
336
+
337
+ // The log is where the api-key shape is recorded; the value never is.
338
+ const fields = soleInjectionMissWarnFields();
339
+ expect(fields.apiKeyShaped).toBe(true);
340
+ expect(fields.policyBlocked).toBe(false);
341
+ expect(fields.missReason).toBeUndefined();
342
+ expect(JSON.stringify(fields)).not.toContain(
343
+ "sk-ant-api03-legacy-bad-value",
344
+ );
278
345
  });
279
346
 
280
347
  test("routes an API key supplied via agent.env (config override) to the missing-token path", async () => {
@@ -304,7 +371,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
304
371
  // be used and the Connect card would re-fire forever. The bad override is
305
372
  // dropped BEFORE the read, so the vault OAuth token is picked up and spawn
306
373
  // proceeds instead of looping.
307
- seedVaultToken("sk-ant-oat01-freshly-connected");
374
+ await seedVaultToken("sk-ant-oat01-freshly-connected");
308
375
 
309
376
  const prepared = await prepareAgentEnv({
310
377
  command: "claude-agent-acp",
@@ -318,7 +385,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
318
385
  });
319
386
 
320
387
  test("keeps a valid OAuth token (sk-ant-oat…) usable (not misclassified)", async () => {
321
- seedVaultToken("sk-ant-oat01-good-token");
388
+ await seedVaultToken("sk-ant-oat01-good-token");
322
389
 
323
390
  const prepared = await prepareAgentEnv({
324
391
  command: "claude-agent-acp",
@@ -331,7 +398,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
331
398
  });
332
399
 
333
400
  test("gates on the resolved command BASENAME (alias to /custom/path/claude-agent-acp still gets the token)", async () => {
334
- seedVaultToken("vault-FFF");
401
+ await seedVaultToken("vault-FFF");
335
402
 
336
403
  const prepared = await prepareAgentEnv({
337
404
  command: "/opt/bin/claude-agent-acp",
@@ -342,7 +409,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
342
409
  });
343
410
 
344
411
  test("does NOT mutate the caller's agentConfig", async () => {
345
- seedVaultToken("vault-GGG");
412
+ await seedVaultToken("vault-GGG");
346
413
  const original = {
347
414
  command: "claude-agent-acp",
348
415
  args: [],
@@ -360,16 +427,16 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
360
427
  });
361
428
 
362
429
  describe("prepareAgentEnv - codex-acp gating", () => {
363
- function seedVaultOpenaiKey(key: string): void {
364
- vaultStore.set("acp/openai_api_key", key);
430
+ function seedVaultOpenaiKey(key: string): Promise<void> {
431
+ return seedVaultValue("openai_api_key", key);
365
432
  }
366
433
 
367
- function seedVaultCodexKey(key: string): void {
368
- vaultStore.set("acp/codex_api_key", key);
434
+ function seedVaultCodexKey(key: string): Promise<void> {
435
+ return seedVaultValue("codex_api_key", key);
369
436
  }
370
437
 
371
438
  test("injects OPENAI_API_KEY from the vault via the broker when agent.env has no override", async () => {
372
- seedVaultOpenaiKey("vault-fake-openai-AAA");
439
+ await seedVaultOpenaiKey("vault-fake-openai-AAA");
373
440
 
374
441
  const prepared = await prepareAgentEnv({
375
442
  command: "codex-acp",
@@ -382,7 +449,7 @@ describe("prepareAgentEnv - codex-acp gating", () => {
382
449
  test("agent.env override wins over the vault entry and skips the broker (precedence pin)", async () => {
383
450
  // Seed a vault value but no metadata: if the override path consulted the
384
451
  // broker anyway, ensureAcpCredentialPolicy would create metadata here.
385
- seedVaultOpenaiKey("vault-fake-openai-BBB");
452
+ await seedVaultOpenaiKey("vault-fake-openai-BBB");
386
453
 
387
454
  const prepared = await prepareAgentEnv({
388
455
  command: "codex-acp",
@@ -391,7 +458,9 @@ describe("prepareAgentEnv - codex-acp gating", () => {
391
458
  });
392
459
 
393
460
  expect(prepared.env?.OPENAI_API_KEY).toBe("config-fake-openai-CCC");
394
- expect(metadataStore.has("acp/openai_api_key")).toBe(false);
461
+ expect(
462
+ getCredentialMetadata(ACP_SERVICE, "openai_api_key"),
463
+ ).toBeUndefined();
395
464
  });
396
465
 
397
466
  test("a vault miss for both fields does NOT throw and spawns with env unchanged (keys are optional)", async () => {
@@ -407,7 +476,7 @@ describe("prepareAgentEnv - codex-acp gating", () => {
407
476
  });
408
477
 
409
478
  test("injects CODEX_API_KEY independently of OPENAI_API_KEY", async () => {
410
- seedVaultCodexKey("vault-fake-codex-DDD");
479
+ await seedVaultCodexKey("vault-fake-codex-DDD");
411
480
 
412
481
  const prepared = await prepareAgentEnv({
413
482
  command: "codex-acp",
@@ -419,8 +488,8 @@ describe("prepareAgentEnv - codex-acp gating", () => {
419
488
  });
420
489
 
421
490
  test("injects both keys when both vault fields are present", async () => {
422
- seedVaultOpenaiKey("vault-fake-openai-EEE");
423
- seedVaultCodexKey("vault-fake-codex-FFF");
491
+ await seedVaultOpenaiKey("vault-fake-openai-EEE");
492
+ await seedVaultCodexKey("vault-fake-codex-FFF");
424
493
 
425
494
  const prepared = await prepareAgentEnv({
426
495
  command: "codex-acp",
@@ -432,7 +501,7 @@ describe("prepareAgentEnv - codex-acp gating", () => {
432
501
  });
433
502
 
434
503
  test("gates on the resolved command BASENAME (custom agent id with full path still gets injection)", async () => {
435
- seedVaultOpenaiKey("vault-fake-openai-GGG");
504
+ await seedVaultOpenaiKey("vault-fake-openai-GGG");
436
505
 
437
506
  const prepared = await prepareAgentEnv({
438
507
  command: "/data/.bun/bin/codex-acp",
@@ -445,7 +514,7 @@ describe("prepareAgentEnv - codex-acp gating", () => {
445
514
 
446
515
  describe("prepareAgentEnv — non-claude commands", () => {
447
516
  test("returns the config unchanged for an unrecognized command basename", async () => {
448
- seedVaultToken("vault-HHH");
517
+ await seedVaultToken("vault-HHH");
449
518
 
450
519
  const prepared = await prepareAgentEnv({
451
520
  command: "some-future-adapter",
@@ -457,38 +526,192 @@ describe("prepareAgentEnv — non-claude commands", () => {
457
526
  });
458
527
  });
459
528
 
460
- describe("grantAcpSpawnPolicy — force-grant (union)", () => {
461
- test("creates metadata with acp_spawn when none exists", () => {
462
- grantAcpSpawnPolicy("claude_oauth_token", "desc");
463
- expect(metadataStore.get("acp/claude_oauth_token")!.allowedTools).toEqual([
464
- "acp_spawn",
465
- ]);
529
+ /**
530
+ * The Connect flow's repair, which `storeAcpClaudeToken` performs after writing
531
+ * the token. Every metadata shape the broker can deny on is covered here, so
532
+ * `acp-claude-oauth.test.ts` only has to pin the end-to-end wiring.
533
+ */
534
+ describe("repairAcpSpawnPolicy - the Connect repair", () => {
535
+ test("creates a record with acp_spawn, the usage description, and no domain restriction when none exists", () => {
536
+ repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
537
+
538
+ expect(oauthMetadata()?.allowedTools).toEqual([ACP_SPAWN_TOOL]);
539
+ expect(oauthMetadata()?.allowedDomains).toEqual([]);
540
+ expect(oauthMetadata()?.usageDescription).toBe("desc");
466
541
  });
467
542
 
468
- test("unions acp_spawn into an explicit policy that omitted it (repair)", () => {
469
- metadataStore.set("acp/claude_oauth_token", {
543
+ test("adds acp_spawn to an empty allowedTools (default provisioning path)", () => {
544
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
545
+ allowedTools: [],
546
+ });
547
+
548
+ repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
549
+
550
+ expect(oauthMetadata()?.allowedTools).toEqual([ACP_SPAWN_TOOL]);
551
+ });
552
+
553
+ test("unions acp_spawn into an explicit policy that omitted it", () => {
554
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
470
555
  allowedTools: ["other_tool"],
471
556
  });
472
557
 
473
- grantAcpSpawnPolicy("claude_oauth_token", "desc");
558
+ repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
474
559
 
475
- // Unlike ensureAcpCredentialPolicy (which preserves), grant adds acp_spawn.
476
- expect(metadataStore.get("acp/claude_oauth_token")!.allowedTools).toEqual([
560
+ // Unlike ensureAcpCredentialPolicy (which preserves), the repair widens.
561
+ expect(oauthMetadata()?.allowedTools).toEqual([
477
562
  "other_tool",
478
- "acp_spawn",
563
+ ACP_SPAWN_TOOL,
479
564
  ]);
480
565
  });
481
566
 
482
- test("leaves an allowedTools that already includes acp_spawn unchanged", () => {
483
- metadataStore.set("acp/claude_oauth_token", {
484
- allowedTools: ["acp_spawn", "other_tool"],
567
+ test("clears a domain restriction the broker refuses server-side, keeping the tools", () => {
568
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
569
+ allowedTools: [ACP_SPAWN_TOOL],
570
+ allowedDomains: ["api.anthropic.com"],
571
+ });
572
+
573
+ repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
574
+
575
+ expect(oauthMetadata()?.allowedDomains).toEqual([]);
576
+ expect(oauthMetadata()?.allowedTools).toEqual([ACP_SPAWN_TOOL]);
577
+ });
578
+
579
+ test("repairs both halves of a denied policy in one pass", () => {
580
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
581
+ allowedTools: ["other_tool"],
582
+ allowedDomains: ["api.anthropic.com"],
485
583
  });
486
584
 
487
- grantAcpSpawnPolicy("claude_oauth_token", "desc");
585
+ repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
488
586
 
489
- expect(metadataStore.get("acp/claude_oauth_token")!.allowedTools).toEqual([
490
- "acp_spawn",
587
+ expect(oauthMetadata()?.allowedTools).toEqual([
491
588
  "other_tool",
589
+ ACP_SPAWN_TOOL,
492
590
  ]);
591
+ expect(oauthMetadata()?.allowedDomains).toEqual([]);
493
592
  });
593
+
594
+ test("writes nothing when the stored policy already satisfies both halves", async () => {
595
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
596
+ allowedTools: [ACP_SPAWN_TOOL, "other_tool"],
597
+ });
598
+ const before = oauthMetadata();
599
+ // Every upsert stamps `updatedAt` with the current millisecond, so waiting
600
+ // here makes an unconditional write show up as a changed record.
601
+ await Bun.sleep(5);
602
+
603
+ repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
604
+
605
+ expect(oauthMetadata()).toEqual(before);
606
+ });
607
+ });
608
+
609
+ /**
610
+ * Drift guard for the Connect Claude status predicate.
611
+ *
612
+ * `acpSpawnCredentialDenialReason` answers "would the spawn's broker read of
613
+ * this credential succeed?" without performing it, because the status route is
614
+ * a side-effect-free GET. Every state below is run twice: once through the
615
+ * predicate, and once through the real spawn sequence
616
+ * (`ensureAcpCredentialPolicy` then `credentialBroker.serverUse`). The two must
617
+ * agree down to the denial string; this suite fails whenever they diverge.
618
+ *
619
+ * The predicate reads only the persisted value via `getSecureKeyAsync` and
620
+ * never calls `serverUse`, so it can never consume a one-time transient
621
+ * credential the way a real broker read would.
622
+ */
623
+ describe("acpSpawnCredentialDenialReason parity with the real spawn read", () => {
624
+ const STATES: { name: string; seed: () => void }[] = [
625
+ { name: "no metadata at all", seed: () => {} },
626
+ {
627
+ name: "empty allowedTools",
628
+ seed: () => {
629
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
630
+ allowedTools: [],
631
+ });
632
+ },
633
+ },
634
+ {
635
+ name: "allowedTools listing acp_spawn",
636
+ seed: () => {
637
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
638
+ allowedTools: [ACP_SPAWN_TOOL],
639
+ });
640
+ },
641
+ },
642
+ {
643
+ name: "allowedTools listing another tool",
644
+ seed: () => {
645
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
646
+ allowedTools: ["bash"],
647
+ });
648
+ },
649
+ },
650
+ {
651
+ // The alias map in tool-policy.ts has no entry canonicalizing to
652
+ // acp_spawn, so this covers alias resolution generically: a legacy alias
653
+ // that canonicalizes to some OTHER capability must still deny.
654
+ name: "allowedTools listing a legacy alias for another capability",
655
+ seed: () => {
656
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
657
+ allowedTools: ["browser_fill_credential"],
658
+ });
659
+ },
660
+ },
661
+ {
662
+ name: "acp_spawn allowed but domain-restricted",
663
+ seed: () => {
664
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
665
+ allowedTools: [ACP_SPAWN_TOOL],
666
+ allowedDomains: ["api.anthropic.com"],
667
+ });
668
+ },
669
+ },
670
+ {
671
+ // The ensure-repair grants acp_spawn but must not drop the domain policy,
672
+ // so the projection has to preserve the rest of the record.
673
+ name: "empty allowedTools and domain-restricted",
674
+ seed: () => {
675
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
676
+ allowedTools: [],
677
+ allowedDomains: ["api.anthropic.com"],
678
+ });
679
+ },
680
+ },
681
+ {
682
+ name: "acp_spawn allowed with an explicitly empty allowedDomains",
683
+ seed: () => {
684
+ upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
685
+ allowedTools: [ACP_SPAWN_TOOL],
686
+ allowedDomains: [],
687
+ });
688
+ },
689
+ },
690
+ ];
691
+
692
+ for (const state of STATES) {
693
+ test(`predicts the spawn read for: ${state.name}`, async () => {
694
+ await seedVaultToken("sk-ant-oat01-parity");
695
+
696
+ state.seed();
697
+ const beforePrediction = oauthMetadata();
698
+ const predicted = acpSpawnCredentialDenialReason(ACP_OAUTH_TOKEN_FIELD);
699
+ // The status route is a GET: predicting must not touch the store.
700
+ expect(oauthMetadata()).toEqual(beforePrediction);
701
+
702
+ // ensureAcpCredentialPolicy mutates the store, so the actual run starts
703
+ // from a fresh copy of the same state rather than the predicted one.
704
+ deleteCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD);
705
+ state.seed();
706
+ ensureAcpCredentialPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
707
+ const result = await credentialBroker.serverUse<void>({
708
+ service: ACP_SERVICE,
709
+ field: ACP_OAUTH_TOKEN_FIELD,
710
+ toolName: ACP_SPAWN_TOOL,
711
+ execute: async () => {},
712
+ });
713
+
714
+ expect(predicted).toBe(result.success ? undefined : result.reason);
715
+ });
716
+ }
494
717
  });