@vellumai/assistant 0.11.4 → 0.11.5-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/conversation-attachments.test.ts +0 -1
  38. package/src/__tests__/conversation-error.test.ts +17 -0
  39. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  40. package/src/__tests__/conversation-pairing.test.ts +186 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  42. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  43. package/src/__tests__/credential-routes.test.ts +22 -3
  44. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  45. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  46. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  47. package/src/__tests__/dm-backfill.test.ts +63 -0
  48. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  49. package/src/__tests__/edit-propagation.test.ts +107 -4
  50. package/src/__tests__/events-client-registration.test.ts +28 -0
  51. package/src/__tests__/file-write-tool.test.ts +4 -2
  52. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  53. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  54. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  55. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  56. package/src/__tests__/host-shell-tool.test.ts +23 -4
  57. package/src/__tests__/image-conversion.test.ts +143 -1
  58. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  59. package/src/__tests__/input-repairs.test.ts +207 -0
  60. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  61. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  62. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  63. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  64. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  65. package/src/__tests__/mtime-cache.test.ts +2 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  67. package/src/__tests__/openai-provider.test.ts +22 -0
  68. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  69. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  70. package/src/__tests__/platform.test.ts +16 -1
  71. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  72. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  73. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  74. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  75. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  76. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  77. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  78. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  79. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  80. package/src/__tests__/reaction-persistence.test.ts +200 -7
  81. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  82. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  83. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  84. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  85. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  86. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  87. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  88. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  89. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  90. package/src/__tests__/tool-executor.test.ts +63 -38
  91. package/src/__tests__/tool-policy.test.ts +97 -1
  92. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  93. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  94. package/src/__tests__/validate-input.test.ts +243 -7
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  97. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  98. package/src/acp/acp-claude-oauth.ts +38 -21
  99. package/src/acp/prepare-agent-env.ts +146 -55
  100. package/src/agent/loop-tool-dedup.test.ts +161 -0
  101. package/src/agent/loop.ts +68 -22
  102. package/src/api/constants/app-tools.ts +34 -0
  103. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  104. package/src/api/index.ts +19 -0
  105. package/src/api/responses/resource-pressure-status.ts +25 -0
  106. package/src/approvals/guardian-channel-delivery.ts +70 -6
  107. package/src/approvals/guardian-request-resolvers.ts +32 -66
  108. package/src/channels/__tests__/message-audience.test.ts +49 -0
  109. package/src/channels/__tests__/types.test.ts +17 -3
  110. package/src/channels/message-audience.ts +40 -0
  111. package/src/channels/types.ts +16 -12
  112. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  113. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  114. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  115. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  116. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  117. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  118. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  119. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  120. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  121. package/src/cli/commands/conversations.help.ts +34 -0
  122. package/src/cli/commands/conversations.ts +125 -0
  123. package/src/cli/commands/inference-providers.ts +9 -4
  124. package/src/cli/commands/inference.help.ts +9 -4
  125. package/src/cli/commands/keys.help.ts +13 -1
  126. package/src/cli/commands/keys.ts +26 -15
  127. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  128. package/src/cli/commands/memory/index.help.ts +41 -27
  129. package/src/cli/commands/memory/index.ts +2 -0
  130. package/src/cli/commands/memory/memory-v2.ts +58 -54
  131. package/src/cli/commands/memory/memory-validate.ts +18 -0
  132. package/src/cli/commands/monitoring.ts +1 -0
  133. package/src/cli/commands/plugins.help.ts +28 -31
  134. package/src/cli/commands/plugins.ts +10 -0
  135. package/src/cli/commands/skills.help.ts +13 -16
  136. package/src/cli/commands/trust.ts +3 -14
  137. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  138. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  139. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  140. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  141. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  142. package/src/cli/lib/bundled-marketplace.json +13 -0
  143. package/src/cli/lib/daemon-credential-client.ts +25 -3
  144. package/src/cli/lib/install-from-github.ts +35 -24
  145. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  146. package/src/cli/lib/plugin-surfaces.ts +27 -0
  147. package/src/config/feature-flag-registry.json +35 -19
  148. package/src/config/webhook-routing.ts +8 -0
  149. package/src/context/compactor.ts +31 -2
  150. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  151. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  152. package/src/daemon/conversation-error.ts +13 -0
  153. package/src/daemon/conversation-messaging.ts +22 -2
  154. package/src/daemon/conversation-process.ts +11 -0
  155. package/src/daemon/conversation-store.ts +10 -0
  156. package/src/daemon/conversation-surfaces.ts +21 -8
  157. package/src/daemon/conversation.ts +12 -10
  158. package/src/daemon/lifecycle.ts +6 -2
  159. package/src/daemon/message-types/conversations.ts +5 -7
  160. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  161. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  162. package/src/daemon/resource-pressure-guard.ts +387 -0
  163. package/src/daemon/shutdown-handlers.ts +2 -0
  164. package/src/daemon/startup-error.ts +16 -0
  165. package/src/daemon/trust-context.ts +14 -10
  166. package/src/daemon/unsendable-image-notice.ts +76 -0
  167. package/src/hooks/registry.ts +65 -51
  168. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  169. package/src/ipc/gateway-client.test.ts +1 -1
  170. package/src/ipc/gateway-client.ts +14 -21
  171. package/src/ipc/socket-cleanup.ts +2 -11
  172. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  173. package/src/messaging/provider-message-metadata.ts +128 -0
  174. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  175. package/src/messaging/providers/channel-transport.ts +90 -13
  176. package/src/messaging/providers/discord/send.ts +16 -0
  177. package/src/messaging/providers/discord/transport.ts +11 -1
  178. package/src/messaging/providers/index.ts +109 -16
  179. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  180. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  181. package/src/messaging/providers/slack/send.test.ts +6 -9
  182. package/src/messaging/providers/slack/send.ts +40 -28
  183. package/src/messaging/providers/slack/transport.ts +22 -26
  184. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  185. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  186. package/src/messaging/read-provider-metadata.test.ts +157 -0
  187. package/src/messaging/read-provider-metadata.ts +51 -0
  188. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  189. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  190. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  191. package/src/notifications/adapters/macos.ts +55 -0
  192. package/src/notifications/adapters/slack.ts +10 -5
  193. package/src/notifications/assistant-reply-producer.ts +37 -4
  194. package/src/notifications/conversation-pairing.ts +111 -21
  195. package/src/notifications/edit-notification.ts +38 -8
  196. package/src/notifications/emit-signal.ts +12 -14
  197. package/src/notifications/home-feed-side-effect.ts +218 -15
  198. package/src/notifications/types.ts +5 -0
  199. package/src/permissions/AGENTS.md +16 -0
  200. package/src/permissions/checker.test.ts +75 -122
  201. package/src/permissions/checker.ts +44 -560
  202. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  203. package/src/permissions/confirmation-guardian-request.ts +5 -1
  204. package/src/persistence/conversation-crud.ts +23 -20
  205. package/src/persistence/conversation-queries.ts +14 -1
  206. package/src/persistence/conversation-types.ts +22 -0
  207. package/src/persistence/delivery-crud.ts +122 -15
  208. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  209. package/src/plugin-api/conversation-turn.ts +27 -0
  210. package/src/plugin-api/credential-scope.test.ts +15 -0
  211. package/src/plugin-api/credential-scope.ts +14 -0
  212. package/src/plugin-api/index.ts +19 -1
  213. package/src/plugin-api/resolve-credential.ts +15 -10
  214. package/src/plugin-api/store-credential.ts +148 -0
  215. package/src/plugin-api/system-card.ts +36 -0
  216. package/src/plugin-api/vision-support.test.ts +82 -0
  217. package/src/plugin-api/vision-support.ts +14 -2
  218. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  219. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  220. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  221. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  222. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  223. package/src/plugins/defaults/main.ts +15 -0
  224. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  225. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  226. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  227. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  228. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  229. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  230. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  231. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  232. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  233. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  234. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  238. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  239. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  240. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  241. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  242. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  243. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  244. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  245. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  246. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  247. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  248. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  249. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  250. package/src/plugins/mtime-cache.ts +1 -1
  251. package/src/plugins/pipeline.ts +14 -7
  252. package/src/plugins/plugin-execution-context.ts +35 -11
  253. package/src/plugins/plugin-tree-walk.ts +63 -4
  254. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  255. package/src/providers/anthropic/client.ts +332 -241
  256. package/src/providers/connection-resolution.ts +2 -1
  257. package/src/providers/content-blocks.ts +22 -0
  258. package/src/providers/gemini/client.ts +5 -2
  259. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  260. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  261. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  262. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  263. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  264. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  265. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  266. package/src/providers/inference/adapter-factory.ts +33 -2
  267. package/src/providers/inference/auth.ts +13 -2
  268. package/src/providers/inference/credential-usage.ts +37 -0
  269. package/src/providers/inference/missing-credential-guard.ts +110 -0
  270. package/src/providers/inference/resolve-auth.ts +7 -7
  271. package/src/providers/media-resolve.ts +176 -15
  272. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  273. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  274. package/src/providers/openai/chat-completions-provider.ts +322 -42
  275. package/src/routes/route-host-protocol.ts +7 -0
  276. package/src/routes/worker.ts +31 -10
  277. package/src/runtime/AGENTS.md +35 -0
  278. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  279. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  280. package/src/runtime/assistant-event-hub.ts +38 -0
  281. package/src/runtime/channel-reply-delivery.ts +54 -31
  282. package/src/runtime/channel-retry-sweep.ts +6 -4
  283. package/src/runtime/effective-capabilities.test.ts +19 -0
  284. package/src/runtime/effective-capabilities.ts +25 -0
  285. package/src/runtime/guardian-reply-router.ts +6 -2
  286. package/src/runtime/http-errors.ts +1 -0
  287. package/src/runtime/http-server.ts +20 -5
  288. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  289. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  290. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  291. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  292. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  293. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  294. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  295. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  296. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  297. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  298. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  299. package/src/runtime/routes/approval-routes.ts +37 -4
  300. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  301. package/src/runtime/routes/canned-message-complete.ts +68 -29
  302. package/src/runtime/routes/client-routes.ts +113 -0
  303. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  304. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  305. package/src/runtime/routes/conversation-routes.ts +36 -22
  306. package/src/runtime/routes/credential-in-use.ts +85 -0
  307. package/src/runtime/routes/credential-routes.ts +55 -85
  308. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  309. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  310. package/src/runtime/routes/identity-routes.ts +5 -121
  311. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  312. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  313. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  314. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  315. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  316. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  317. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  318. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  319. package/src/runtime/routes/index.ts +2 -0
  320. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  321. package/src/runtime/routes/monitoring-routes.ts +9 -1
  322. package/src/runtime/routes/plugins-routes.ts +17 -5
  323. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  324. package/src/runtime/routes/secret-routes.ts +39 -8
  325. package/src/runtime/routes/settings-routes.ts +6 -6
  326. package/src/runtime/routes/surface-action-routes.ts +20 -19
  327. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  328. package/src/runtime/routes/user-route-resolution.ts +18 -1
  329. package/src/runtime/slack-reply-session.test.ts +23 -13
  330. package/src/runtime/slack-reply-session.ts +38 -55
  331. package/src/runtime/web-presence.ts +90 -0
  332. package/src/skills/validate-input.ts +177 -40
  333. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  334. package/src/subagent/consult-context.ts +7 -23
  335. package/src/tools/credentials/broker.ts +24 -79
  336. package/src/tools/credentials/ref-parse.ts +35 -0
  337. package/src/tools/credentials/resolve.ts +4 -10
  338. package/src/tools/credentials/store.ts +168 -0
  339. package/src/tools/credentials/tool-policy.ts +67 -0
  340. package/src/tools/executor.ts +53 -15
  341. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  342. package/src/tools/network/url-safety.ts +9 -16
  343. package/src/tools/network/web-search.ts +21 -3
  344. package/src/tools/permission-checker.ts +43 -52
  345. package/src/tools/schema-transforms.ts +40 -5
  346. package/src/tools/shared/input-repairs.ts +160 -0
  347. package/src/tools/skills/skill-tool-factory.ts +18 -4
  348. package/src/tools/subagent/spawn.ts +0 -1
  349. package/src/tools/tool-approval-handler.ts +16 -10
  350. package/src/tools/tool-types.ts +26 -13
  351. package/src/tools/types.ts +6 -6
  352. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  353. package/src/util/cgroup-memory.ts +3 -0
  354. package/src/util/container-cpu-sampler.ts +250 -0
  355. package/src/util/image-conversion.ts +178 -14
  356. package/src/util/platform.ts +93 -10
  357. package/src/permissions/ipc-risk-types.ts +0 -143
  358. package/src/permissions/risk-types.ts +0 -76
@@ -43,8 +43,11 @@ let checkFnOverride:
43
43
  /** Override for generateScopeOptions — when set, returns this value instead of the default. */
44
44
  let scopeOptionsOverride: ScopeOption[] | undefined;
45
45
 
46
- /** Override for getCachedAssessment — when set, returns this value. */
47
- let cachedAssessmentOverride:
46
+ /**
47
+ * Override for the classification `classifyRisk` returns; when unset the
48
+ * classifier answers low with no reason and no options.
49
+ */
50
+ let classificationOverride:
48
51
  | {
49
52
  riskLevel: string;
50
53
  reason: string;
@@ -61,7 +64,14 @@ let cachedAssessmentOverride:
61
64
 
62
65
  mock.module("../permissions/checker.js", () => ({
63
66
  isDynamicSkillLoadInvocation: () => false,
64
- classifyRisk: async () => ({ level: "low" }),
67
+ classifyRisk: async () => ({
68
+ level: classificationOverride?.riskLevel ?? "low",
69
+ reason: classificationOverride?.reason,
70
+ scopeOptions: classificationOverride?.scopeOptions ?? [],
71
+ allowlistOptions: classificationOverride?.allowlistOptions,
72
+ directoryScopeOptions: classificationOverride?.directoryScopeOptions,
73
+ matchType: classificationOverride?.matchType ?? "unknown",
74
+ }),
65
75
  check: async (
66
76
  toolName: string,
67
77
  input: Record<string, unknown>,
@@ -77,12 +87,8 @@ mock.module("../permissions/checker.js", () => ({
77
87
  }
78
88
  return { decision: "allow", reason: "allowed" };
79
89
  },
80
- generateAllowlistOptions: () => [
81
- { label: "exact", description: "exact", pattern: "exact" },
82
- ],
83
90
  generateScopeOptions: () =>
84
91
  scopeOptionsOverride ?? [{ label: "/tmp", scope: "/tmp" }],
85
- getCachedAssessment: () => cachedAssessmentOverride,
86
92
  }));
87
93
 
88
94
  // Mock every export so downstream test files that dynamically import modules
@@ -167,7 +173,7 @@ describe("ToolExecutor allowedToolNames gating", () => {
167
173
  getAllToolsOverride = undefined;
168
174
  checkResultOverride = undefined;
169
175
  checkFnOverride = undefined;
170
- cachedAssessmentOverride = undefined;
176
+ classificationOverride = undefined;
171
177
  });
172
178
 
173
179
  test("executes normally when allowedToolNames is not set", async () => {
@@ -385,7 +391,7 @@ describe("ToolExecutor policy context plumbing", () => {
385
391
  getToolOverride = undefined;
386
392
  checkResultOverride = undefined;
387
393
  checkFnOverride = undefined;
388
- cachedAssessmentOverride = undefined;
394
+ classificationOverride = undefined;
389
395
  });
390
396
 
391
397
  test("passes PolicyContext with executionTarget for skill-origin tools", async () => {
@@ -604,7 +610,7 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
604
610
  getToolOverride = undefined;
605
611
  checkResultOverride = undefined;
606
612
  checkFnOverride = undefined;
607
- cachedAssessmentOverride = undefined;
613
+ classificationOverride = undefined;
608
614
  promptCalled = false;
609
615
  });
610
616
 
@@ -931,11 +937,30 @@ describe("integration regressions — prompt payload (PR 11)", () => {
931
937
  getToolOverride = undefined;
932
938
  });
933
939
 
934
- test("shell command prompt payload includes allowlist and scope options", async () => {
940
+ test("shell command prompt payload carries the classifier's ladder and the scope options", async () => {
935
941
  checkResultOverride = {
936
942
  decision: "prompt",
937
943
  reason: "Medium risk: requires approval",
938
944
  };
945
+ // The ladder is the gateway's; the daemon passes it through untouched.
946
+ classificationOverride = {
947
+ riskLevel: "medium",
948
+ reason: "npm install",
949
+ scopeOptions: [],
950
+ allowlistOptions: [
951
+ {
952
+ label: "npm install",
953
+ description: "Exact command",
954
+ pattern: "npm install",
955
+ },
956
+ {
957
+ label: "action:npm",
958
+ description: "Any npm command",
959
+ pattern: "action:npm",
960
+ },
961
+ ],
962
+ matchType: "registry",
963
+ };
939
964
 
940
965
  let capturedAllowlist: AllowlistOption[] | undefined;
941
966
  let capturedScopes: ScopeOption[] | undefined;
@@ -963,13 +988,11 @@ describe("integration regressions — prompt payload (PR 11)", () => {
963
988
  makeContext({ forcePromptSideEffects: true }),
964
989
  );
965
990
 
966
- // Verify that the prompter received allowlist options
967
- expect(capturedAllowlist).toBeDefined();
968
- expect(capturedAllowlist!.length).toBeGreaterThan(0);
969
- // The mock returns [{label: 'exact', description: 'exact', pattern: 'exact'}]
970
- expect(capturedAllowlist![0]).toHaveProperty("pattern");
971
- expect(capturedAllowlist![0]).toHaveProperty("label");
972
- expect(capturedAllowlist![0]).toHaveProperty("description");
991
+ // The prompt offers exactly what the classifier produced, in order.
992
+ expect(capturedAllowlist?.map((o) => o.pattern)).toEqual([
993
+ "npm install",
994
+ "action:npm",
995
+ ]);
973
996
 
974
997
  // Verify scope options are also passed
975
998
  expect(capturedScopes).toBeDefined();
@@ -989,11 +1012,11 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
989
1012
  getToolOverride = undefined;
990
1013
  checkResultOverride = undefined;
991
1014
  checkFnOverride = undefined;
992
- cachedAssessmentOverride = undefined;
1015
+ classificationOverride = undefined;
993
1016
  });
994
1017
 
995
1018
  test("auto-approved tool result includes risk metadata when classifier assessment exists", async () => {
996
- cachedAssessmentOverride = {
1019
+ classificationOverride = {
997
1020
  riskLevel: "medium",
998
1021
  reason: "Writes to a file outside the workspace",
999
1022
  scopeOptions: [
@@ -1019,8 +1042,10 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1019
1042
  ]);
1020
1043
  });
1021
1044
 
1022
- test("tool result omits risk metadata when no classifier assessment exists (e.g. MCP tools)", async () => {
1023
- // cachedAssessmentOverride is undefined (no classifier ran)
1045
+ test("tool result carries the classification's risk metadata even when the classifier produced no options", async () => {
1046
+ // classificationOverride is unset: the classifier answered low with no
1047
+ // reason and no options, which is what a tool with no dedicated classifier
1048
+ // (registry default risk) looks like.
1024
1049
  const executor = new ToolExecutor(makePrompter());
1025
1050
  const result = await executor.execute(
1026
1051
  "file_read",
@@ -1029,9 +1054,9 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1029
1054
  );
1030
1055
 
1031
1056
  expect(result.isError).toBe(false);
1032
- expect(result.riskLevel).toBeUndefined();
1033
- expect(result.riskReason).toBeUndefined();
1034
- expect(result.riskScopeOptions).toBeUndefined();
1057
+ expect(result.riskLevel).toBe("low");
1058
+ expect(result.riskScopeOptions).toEqual([]);
1059
+ expect(result.riskAllowlistOptions).toBeUndefined();
1035
1060
  });
1036
1061
 
1037
1062
  test("denied tool result includes risk metadata", async () => {
@@ -1039,7 +1064,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1039
1064
  decision: "deny",
1040
1065
  reason: "Blocked by deny rule",
1041
1066
  };
1042
- cachedAssessmentOverride = {
1067
+ classificationOverride = {
1043
1068
  riskLevel: "high",
1044
1069
  reason: "Recursive force delete",
1045
1070
  scopeOptions: [{ pattern: "bash:rm -rf*", label: "rm -rf commands" }],
@@ -1066,7 +1091,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1066
1091
  decision: "prompt",
1067
1092
  reason: "Medium risk: requires approval",
1068
1093
  };
1069
- cachedAssessmentOverride = {
1094
+ classificationOverride = {
1070
1095
  riskLevel: "medium",
1071
1096
  reason: "Package manager installation",
1072
1097
  scopeOptions: [
@@ -1091,7 +1116,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1091
1116
  });
1092
1117
 
1093
1118
  test("tool result includes riskDirectoryScopeOptions when classifier emits directoryScopeOptions", async () => {
1094
- cachedAssessmentOverride = {
1119
+ classificationOverride = {
1095
1120
  riskLevel: "medium",
1096
1121
  reason: "Writes to file in workspace",
1097
1122
  scopeOptions: [
@@ -1124,7 +1149,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1124
1149
  });
1125
1150
 
1126
1151
  test("tool result omits riskDirectoryScopeOptions when classifier does not emit directoryScopeOptions", async () => {
1127
- cachedAssessmentOverride = {
1152
+ classificationOverride = {
1128
1153
  riskLevel: "low",
1129
1154
  reason: "Read-only operation",
1130
1155
  scopeOptions: [],
@@ -1144,7 +1169,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1144
1169
  });
1145
1170
 
1146
1171
  test("riskScopeOptions and riskDirectoryScopeOptions are independent — one does not clobber the other", async () => {
1147
- cachedAssessmentOverride = {
1172
+ classificationOverride = {
1148
1173
  riskLevel: "medium",
1149
1174
  reason: "Filesystem write",
1150
1175
  scopeOptions: [
@@ -1170,7 +1195,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1170
1195
  });
1171
1196
 
1172
1197
  test("auto-approved tool result includes riskAllowlistOptions when classifier emits them (Minimatch-glob shape for save path)", async () => {
1173
- cachedAssessmentOverride = {
1198
+ classificationOverride = {
1174
1199
  riskLevel: "medium",
1175
1200
  reason: "Reads workspace files",
1176
1201
  // Display ladder (regex shape — not for save).
@@ -1222,7 +1247,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1222
1247
  });
1223
1248
 
1224
1249
  test("riskAllowlistOptions is undefined when classifier did not produce allowlist (e.g. web-risk classifier)", async () => {
1225
- cachedAssessmentOverride = {
1250
+ classificationOverride = {
1226
1251
  riskLevel: "low",
1227
1252
  reason: "GET request to public URL",
1228
1253
  scopeOptions: [
@@ -1248,8 +1273,8 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1248
1273
  expect(result.riskAllowlistOptions).toBeUndefined();
1249
1274
  });
1250
1275
 
1251
- test("riskAllowlistOptions is undefined when no classifier ran (MCP tools)", async () => {
1252
- // cachedAssessmentOverride is undefined — no classifier ran.
1276
+ test("riskAllowlistOptions is undefined when the classifier produced none", async () => {
1277
+ // classificationOverride is unset: no allowlist options on the answer.
1253
1278
  const executor = new ToolExecutor(makePrompter());
1254
1279
  const result = await executor.execute(
1255
1280
  "file_read",
@@ -1258,13 +1283,13 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
1258
1283
  );
1259
1284
 
1260
1285
  expect(result.isError).toBe(false);
1261
- expect(result.riskScopeOptions).toBeUndefined();
1286
+ expect(result.riskScopeOptions).toEqual([]);
1262
1287
  expect(result.riskAllowlistOptions).toBeUndefined();
1263
1288
  });
1264
1289
 
1265
1290
  test("denied tool result still carries riskAllowlistOptions for the rule editor save path", async () => {
1266
1291
  checkResultOverride = { decision: "deny", reason: "Blocked by deny rule" };
1267
- cachedAssessmentOverride = {
1292
+ classificationOverride = {
1268
1293
  riskLevel: "high",
1269
1294
  reason: "Recursive force delete",
1270
1295
  scopeOptions: [{ pattern: "^rm\\s+-rf", label: "rm -rf *" }],
@@ -1334,7 +1359,7 @@ describe("ToolExecutor thrown-value rendering", () => {
1334
1359
  getAllToolsOverride = undefined;
1335
1360
  checkResultOverride = undefined;
1336
1361
  checkFnOverride = undefined;
1337
- cachedAssessmentOverride = undefined;
1362
+ classificationOverride = undefined;
1338
1363
  });
1339
1364
 
1340
1365
  function throwingTool(name: string, thrown: unknown): void {
@@ -1406,7 +1431,7 @@ describe("ToolExecutor input-schema validation gate", () => {
1406
1431
  getAllToolsOverride = undefined;
1407
1432
  checkResultOverride = undefined;
1408
1433
  checkFnOverride = undefined;
1409
- cachedAssessmentOverride = undefined;
1434
+ classificationOverride = undefined;
1410
1435
  });
1411
1436
 
1412
1437
  /**
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { isToolAllowed } from "../tools/credentials/tool-policy.js";
3
+ import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
4
+ import {
5
+ BROWSER_FILL_CAPABILITY,
6
+ isToolAllowed,
7
+ serverUseDenialReason,
8
+ } from "../tools/credentials/tool-policy.js";
4
9
 
5
10
  describe("isToolAllowed", () => {
6
11
  // ── Allow cases ─────────────────────────────────────────────────────
@@ -72,3 +77,94 @@ describe("isToolAllowed", () => {
72
77
  ).toBe(false);
73
78
  });
74
79
  });
80
+
81
+ describe("serverUseDenialReason", () => {
82
+ function metadata(
83
+ overrides: Partial<CredentialMetadata> = {},
84
+ ): CredentialMetadata {
85
+ return {
86
+ credentialId: "cred-1",
87
+ service: "acp",
88
+ field: "claude_oauth_token",
89
+ allowedTools: ["acp_spawn"],
90
+ allowedDomains: [],
91
+ createdAt: 0,
92
+ updatedAt: 0,
93
+ ...overrides,
94
+ };
95
+ }
96
+
97
+ test("denies when metadata is missing", () => {
98
+ expect(
99
+ serverUseDenialReason(
100
+ undefined,
101
+ "acp_spawn",
102
+ "acp",
103
+ "claude_oauth_token",
104
+ ),
105
+ ).toBe("No credential found for acp/claude_oauth_token");
106
+ });
107
+
108
+ test("denies fail-closed when allowedTools is empty", () => {
109
+ const reason = serverUseDenialReason(
110
+ metadata({ allowedTools: [] }),
111
+ "acp_spawn",
112
+ "acp",
113
+ "claude_oauth_token",
114
+ );
115
+ expect(reason).toContain(
116
+ 'Tool "acp_spawn" is not allowed to use credential acp/claude_oauth_token.',
117
+ );
118
+ expect(reason).toContain("No tools are currently allowed");
119
+ });
120
+
121
+ test("denies when allowedTools omits the requesting tool", () => {
122
+ const reason = serverUseDenialReason(
123
+ metadata({ allowedTools: ["bash", "web_fetch"] }),
124
+ "acp_spawn",
125
+ "acp",
126
+ "claude_oauth_token",
127
+ );
128
+ expect(reason).toContain(
129
+ 'Tool "acp_spawn" is not allowed to use credential acp/claude_oauth_token.',
130
+ );
131
+ expect(reason).toContain("Allowed tools: bash, web_fetch.");
132
+ });
133
+
134
+ test("allows when a legacy alias canonicalizes to the requesting tool", () => {
135
+ expect(
136
+ serverUseDenialReason(
137
+ metadata({ allowedTools: ["browser_fill_credential"] }),
138
+ BROWSER_FILL_CAPABILITY,
139
+ "acp",
140
+ "claude_oauth_token",
141
+ ),
142
+ ).toBeUndefined();
143
+ });
144
+
145
+ test("denies a domain-restricted credential", () => {
146
+ expect(
147
+ serverUseDenialReason(
148
+ metadata({ allowedDomains: ["example.com", "example.org"] }),
149
+ "acp_spawn",
150
+ "acp",
151
+ "claude_oauth_token",
152
+ ),
153
+ ).toBe(
154
+ "Credential acp/claude_oauth_token has domain restrictions " +
155
+ "(example.com, example.org) and cannot be used server-side. " +
156
+ "Remove domain restrictions or use a separate credential without domain policy.",
157
+ );
158
+ });
159
+
160
+ test("allows fully-permitted metadata", () => {
161
+ expect(
162
+ serverUseDenialReason(
163
+ metadata(),
164
+ "acp_spawn",
165
+ "acp",
166
+ "claude_oauth_token",
167
+ ),
168
+ ).toBeUndefined();
169
+ });
170
+ });
@@ -1231,8 +1231,9 @@ describe("(g) access_request resolver: requester code delivery", () => {
1231
1231
  expect(requesterCodeReply!.payload.text).toContain(
1232
1232
  "your access request was approved",
1233
1233
  );
1234
- // The code DM is durable, never ephemeral.
1235
- expect(requesterCodeReply!.payload.ephemeral).toBeUndefined();
1234
+ // The code DM is durable, so it carries no audience: restricting it
1235
+ // would cost durability without hiding it from anyone.
1236
+ expect(requesterCodeReply!.payload.audience).toBeUndefined();
1236
1237
  // threadTs (the guardian's channel thread) is stripped for the DM.
1237
1238
  expect(requesterCodeReply!.url).not.toContain("threadTs");
1238
1239
 
@@ -1411,8 +1412,10 @@ describe("(g) access_request resolver: requester code delivery", () => {
1411
1412
  );
1412
1413
  expect(courier).toBeDefined();
1413
1414
  expect(courier!.payload.chatId).toBe("C_SHARED_CHANNEL");
1414
- expect(courier!.payload.ephemeral).toBe(true);
1415
- expect(courier!.payload.user).toBe(REQUESTER_UID);
1415
+ expect(courier!.payload.audience).toEqual({
1416
+ kind: "oneReader",
1417
+ userId: REQUESTER_UID,
1418
+ });
1416
1419
  });
1417
1420
 
1418
1421
  test("guardian-facing reply uses the requester's display name, not the raw ID", async () => {
@@ -28,6 +28,7 @@ import {
28
28
  getCachedUserTools,
29
29
  resetPluginCacheForTests,
30
30
  } from "../plugins/mtime-cache.js";
31
+ import { resetPluginRegistryForTests } from "../plugins/registry.js";
31
32
  import { loadUserPlugins } from "../plugins/user-loader.js";
32
33
 
33
34
  // Isolate every run under its own tempdir so parallel test files (and
@@ -71,6 +72,7 @@ function clearPluginsDir(): void {
71
72
  describe("user plugin loader", () => {
72
73
  beforeEach(() => {
73
74
  resetPluginCacheForTests();
75
+ resetPluginRegistryForTests();
74
76
  clearPluginsDir();
75
77
  });
76
78
 
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
+ coerceArrayShapes,
4
5
  coerceStringBooleans,
5
6
  validateInputAgainstSchema,
6
7
  } from "../skills/validate-input.js";
@@ -450,20 +451,255 @@ describe("coerceStringBooleans", () => {
450
451
  });
451
452
  });
452
453
 
453
- describe("validateInputAgainstSchema — boolean string error message", () => {
454
- test("points at the JSON boolean form when a string is passed", () => {
454
+ describe("coerceArrayShapes", () => {
455
+ const schema = {
456
+ type: "object",
457
+ properties: {
458
+ activation_hints: { type: "array", items: { type: "string" } },
459
+ files: {
460
+ type: "array",
461
+ items: {
462
+ type: "object",
463
+ properties: {
464
+ path: { type: "string" },
465
+ content: { type: "string" },
466
+ copy_from: { type: "string" },
467
+ },
468
+ required: ["path"],
469
+ },
470
+ },
471
+ steps: {
472
+ type: "array",
473
+ items: {
474
+ type: "object",
475
+ properties: {
476
+ action: { type: "string" },
477
+ require_approval: { type: "boolean" },
478
+ },
479
+ },
480
+ },
481
+ name: { type: "string" },
482
+ tags: { type: ["array", "null"] },
483
+ untyped: { type: "array" },
484
+ },
485
+ };
486
+
487
+ describe("a string holding the JSON text of the array", () => {
488
+ test("decodes an array of strings", () => {
489
+ const result = coerceArrayShapes(
490
+ { activation_hints: '["deploy staging","roll back"]' },
491
+ schema,
492
+ );
493
+ expect(result.activation_hints).toEqual(["deploy staging", "roll back"]);
494
+ });
495
+
496
+ test("decodes an array of objects", () => {
497
+ const result = coerceArrayShapes(
498
+ { files: '[{"path":"references/notes.md","content":"hi"}]' },
499
+ schema,
500
+ );
501
+ expect(result.files).toEqual([
502
+ { path: "references/notes.md", content: "hi" },
503
+ ]);
504
+ });
505
+
506
+ test("decodes for an array property that declares no item type", () => {
507
+ const result = coerceArrayShapes({ untyped: '["a"]' }, schema);
508
+ expect(result.untyped).toEqual(["a"]);
509
+ });
510
+ });
511
+
512
+ describe("a single element where a list was expected", () => {
513
+ test("wraps a bare phrase for a string-item array", () => {
514
+ const result = coerceArrayShapes(
515
+ { activation_hints: "user asks to deploy staging" },
516
+ schema,
517
+ );
518
+ expect(result.activation_hints).toEqual(["user asks to deploy staging"]);
519
+ });
520
+
521
+ test("wraps the decoded text of a JSON-quoted phrase", () => {
522
+ const result = coerceArrayShapes(
523
+ { activation_hints: '"user asks to deploy staging"' },
524
+ schema,
525
+ );
526
+ expect(result.activation_hints).toEqual(["user asks to deploy staging"]);
527
+ });
528
+
529
+ test("wraps one object that spells out the item schema's properties", () => {
530
+ const result = coerceArrayShapes(
531
+ { files: { path: "references/notes.md", content: "hi" } },
532
+ schema,
533
+ );
534
+ expect(result.files).toEqual([
535
+ { path: "references/notes.md", content: "hi" },
536
+ ]);
537
+ });
538
+
539
+ test("leaves an object keyed by something the item schema does not name", () => {
540
+ const input = { files: { "references/notes.md": "hi" } };
541
+ const result = coerceArrayShapes(input, schema);
542
+ expect(result).toBe(input);
543
+ });
544
+
545
+ test("leaves a bare phrase for an object-item array", () => {
546
+ const input = { files: "references/notes.md" };
547
+ const result = coerceArrayShapes(input, schema);
548
+ expect(result).toBe(input);
549
+ });
550
+
551
+ test("leaves a bare phrase when the array declares no item type", () => {
552
+ const input = { untyped: "one thing" };
553
+ expect(coerceArrayShapes(input, schema)).toBe(input);
554
+ });
555
+ });
556
+
557
+ describe("shapes left for the validator to reject", () => {
558
+ test.each([
559
+ ['["deploy staging"', "a truncated array"],
560
+ ['{"path":"references/notes.md"', "a truncated object"],
561
+ ["", "an empty string"],
562
+ [" ", "blank text"],
563
+ ])("leaves %p (%s)", (raw) => {
564
+ const input = { activation_hints: raw };
565
+ const result = coerceArrayShapes(input, schema);
566
+ expect(result).toBe(input);
567
+ const validation = validateInputAgainstSchema(
568
+ "scaffold_managed_skill",
569
+ result,
570
+ schema,
571
+ );
572
+ expect(validation.ok).toBe(false);
573
+ });
574
+ });
575
+
576
+ test("repaired input passes validation and preserves intent", () => {
577
+ const coerced = coerceArrayShapes(
578
+ {
579
+ activation_hints: "user asks to deploy staging",
580
+ files: { path: "references/notes.md", content: "hi" },
581
+ name: "x",
582
+ },
583
+ schema,
584
+ );
585
+ expect(
586
+ validateInputAgainstSchema("scaffold_managed_skill", coerced, schema),
587
+ ).toEqual({ ok: true });
588
+ expect(coerced.activation_hints).toEqual(["user asks to deploy staging"]);
589
+ });
590
+
591
+ describe("elements an array gains here", () => {
592
+ test("are coerced against the item schema when decoded", () => {
593
+ const result = coerceArrayShapes(
594
+ { steps: '[{"action":"send","require_approval":"false"}]' },
595
+ schema,
596
+ );
597
+ expect(result.steps).toEqual([
598
+ { action: "send", require_approval: false },
599
+ ]);
600
+ });
601
+
602
+ test("are coerced against the item schema when wrapped", () => {
603
+ const result = coerceArrayShapes(
604
+ { steps: { action: "send", require_approval: "true" } },
605
+ schema,
606
+ );
607
+ expect(result.steps).toEqual([
608
+ { action: "send", require_approval: true },
609
+ ]);
610
+ });
611
+
612
+ test("leave non-object elements alone", () => {
613
+ const result = coerceArrayShapes({ steps: '["send",7]' }, schema);
614
+ expect(result.steps).toEqual(["send", 7]);
615
+ });
616
+ });
617
+
618
+ test("still reports a wrong element type after repair", () => {
619
+ const coerced = coerceArrayShapes(
620
+ { activation_hints: '["deploy staging",7]' },
621
+ schema,
622
+ );
623
+ const validation = validateInputAgainstSchema(
624
+ "scaffold_managed_skill",
625
+ coerced,
626
+ schema,
627
+ );
628
+ expect(validation.ok).toBe(false);
629
+ if (validation.ok) {
630
+ return;
631
+ }
632
+ expect(validation.errors).toContain("activation_hints[1] must be a string");
633
+ });
634
+
635
+ test("does not touch non-array-typed or union-typed properties", () => {
636
+ const input = { name: '["x"]', tags: '["x"]' };
637
+ const result = coerceArrayShapes(input, schema);
638
+ expect(result).toBe(input);
639
+ expect(result.name).toBe('["x"]');
640
+ expect(result.tags).toBe('["x"]');
641
+ });
642
+
643
+ test("does not touch real arrays or other types", () => {
644
+ const input = { activation_hints: ["deploy staging"] };
645
+ expect(coerceArrayShapes(input, schema)).toBe(input);
646
+ const inputNum = { activation_hints: 7 };
647
+ expect(coerceArrayShapes(inputNum, schema)).toBe(inputNum);
648
+ });
649
+
650
+ test("returns the same object when there is nothing to repair", () => {
651
+ const input = { name: "x" };
652
+ expect(coerceArrayShapes(input, schema)).toBe(input);
653
+ expect(coerceArrayShapes(input, undefined)).toBe(input);
654
+ expect(coerceArrayShapes(input, { type: "object" })).toBe(input);
655
+ });
656
+
657
+ test("returns a new object and never mutates the original input", () => {
658
+ const input = { activation_hints: '["deploy staging"]', name: "x" };
659
+ const snapshot = JSON.parse(JSON.stringify(input));
660
+ const result = coerceArrayShapes(input, schema);
661
+ expect(result).not.toBe(input);
662
+ expect(input).toEqual(snapshot);
663
+ expect(result.name).toBe("x");
664
+ });
665
+ });
666
+
667
+ describe("validateInputAgainstSchema: array shape error message", () => {
668
+ const schema = {
669
+ type: "object",
670
+ properties: {
671
+ activation_hints: { type: "array", items: { type: "string" } },
672
+ },
673
+ };
674
+
675
+ test("points at the JSON array form when a string is passed", () => {
455
676
  const result = validateInputAgainstSchema(
456
- "app_create",
457
- { auto_open: "yes" },
458
- { type: "object", properties: { auto_open: { type: "boolean" } } },
677
+ "scaffold_managed_skill",
678
+ { activation_hints: "[deploy staging" },
679
+ schema,
459
680
  );
460
681
  expect(result.ok).toBe(false);
461
682
  if (result.ok) {
462
683
  return;
463
684
  }
464
- expect(result.errors).toContain(
465
- "auto_open must be a boolean — pass true or false as a JSON boolean, not a string",
685
+ expect(result.errors).toEqual([
686
+ "activation_hints must be an array: pass a JSON array, not a string",
687
+ ]);
688
+ });
689
+
690
+ test("names an object when an object is passed", () => {
691
+ const result = validateInputAgainstSchema(
692
+ "scaffold_managed_skill",
693
+ { activation_hints: { first: "deploy staging" } },
694
+ schema,
466
695
  );
696
+ expect(result.ok).toBe(false);
697
+ if (result.ok) {
698
+ return;
699
+ }
700
+ expect(result.errors).toEqual([
701
+ "activation_hints must be an array: pass a JSON array, not an object",
702
+ ]);
467
703
  });
468
704
  });
469
705