@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
@@ -6,13 +6,16 @@
6
6
  * gated on darwin.
7
7
  */
8
8
 
9
- import { mkdtempSync, writeFileSync } from "node:fs";
9
+ import { createHash } from "node:crypto";
10
+ import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
10
11
  import { tmpdir } from "node:os";
11
12
  import { join } from "node:path";
12
13
  import { beforeAll, describe, expect, test } from "bun:test";
13
14
 
14
15
  import {
15
16
  convertImageToJpeg,
17
+ hasValidJpegStructure,
18
+ isCompleteJpeg,
16
19
  isHeifImage,
17
20
  jpegFilenameFor,
18
21
  normalizeImageBase64,
@@ -131,6 +134,119 @@ describe("sniffImageMimeType", () => {
131
134
  });
132
135
  });
133
136
 
137
+ describe("isCompleteJpeg", () => {
138
+ test("accepts SOI...EOI framed payloads", () => {
139
+ expect(
140
+ isCompleteJpeg(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0xff, 0xd9])),
141
+ ).toBe(true);
142
+ });
143
+
144
+ test("rejects empty, truncated, and non-JPEG payloads", () => {
145
+ expect(isCompleteJpeg(Buffer.alloc(0))).toBe(false);
146
+ // Valid head, torn tail: the poisoned-cache signature.
147
+ expect(isCompleteJpeg(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]))).toBe(
148
+ false,
149
+ );
150
+ expect(isCompleteJpeg(PNG_1PX_BYTES)).toBe(false);
151
+ });
152
+ });
153
+
154
+ // Mirrors the converter's cache-key derivation so the test can plant a
155
+ // poisoned entry at the exact path a conversion will consult.
156
+ function cacheKeyFor(bytes: Uint8Array, quality: number): string {
157
+ const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 16);
158
+ return `${hash}-full-q${quality}`;
159
+ }
160
+
161
+ // Minimal structurally valid JPEG: SOI, APP0 (len 4, 2 payload bytes),
162
+ // SOF0 (len 5, 3 payload bytes), SOS (len 3, 1 payload byte), entropy data
163
+ // with a stuffed FF, then EOI.
164
+ function minimalJpeg(extra: {
165
+ trailing?: Uint8Array;
166
+ dropEoi?: boolean;
167
+ }): Buffer {
168
+ const parts: Uint8Array[] = [
169
+ Buffer.from([0xff, 0xd8]), // SOI
170
+ Buffer.from([0xff, 0xe0, 0x00, 0x04, 0x4a, 0x46]), // APP0
171
+ Buffer.from([0xff, 0xc0, 0x00, 0x05, 0x08, 0x00, 0x01]), // SOF0
172
+ Buffer.from([0xff, 0xda, 0x00, 0x03, 0x01]), // SOS header
173
+ Buffer.from([0x12, 0xff, 0x00, 0x34]), // entropy data with stuffed FF
174
+ ];
175
+ if (!extra.dropEoi) {
176
+ parts.push(Buffer.from([0xff, 0xd9])); // EOI
177
+ }
178
+ if (extra.trailing) {
179
+ parts.push(extra.trailing);
180
+ }
181
+ return Buffer.concat(parts);
182
+ }
183
+
184
+ describe("hasValidJpegStructure", () => {
185
+ test("accepts a JPEG with trailing bytes after the EOI marker", () => {
186
+ // Some encoders append padding or metadata past the EOI; the structural
187
+ // walk tolerates it where isCompleteJpeg's exact tail framing does not.
188
+ const padded = minimalJpeg({ trailing: Buffer.from([0x00, 0x00]) });
189
+ expect(hasValidJpegStructure(padded)).toBe(true);
190
+ expect(isCompleteJpeg(padded)).toBe(false);
191
+ });
192
+
193
+ test("accepts restart markers inside entropy-coded data", () => {
194
+ const withRst = Buffer.concat([
195
+ Buffer.from([0xff, 0xd8]),
196
+ Buffer.from([0xff, 0xc0, 0x00, 0x05, 0x08, 0x00, 0x01]), // SOF0
197
+ Buffer.from([0xff, 0xda, 0x00, 0x03, 0x01]),
198
+ Buffer.from([0x12, 0xff, 0xd0, 0x34]), // RST0 continues the scan
199
+ Buffer.from([0xff, 0xd9]),
200
+ ]);
201
+ expect(hasValidJpegStructure(withRst)).toBe(true);
202
+ });
203
+
204
+ test("rejects a truncated JPEG whose metadata segment contains FF D9", () => {
205
+ // An EXIF-style APP1 payload can embed a complete thumbnail JPEG, so an
206
+ // FF D9 pair appears inside the segment while the main image is torn. A
207
+ // raw byte search would accept this; the marker walk skips the segment
208
+ // payload and reports the truncation.
209
+ const tornWithThumbnail = Buffer.concat([
210
+ Buffer.from([0xff, 0xd8]), // SOI
211
+ Buffer.from([0xff, 0xe1, 0x00, 0x08]), // APP1, length 8
212
+ Buffer.from([0x45, 0x78, 0xff, 0xd9, 0x00, 0x00]), // payload with FF D9
213
+ Buffer.from([0xff, 0xda, 0x00, 0x03, 0x01, 0x12, 0x34]), // torn scan
214
+ ]);
215
+ expect(hasValidJpegStructure(tornWithThumbnail)).toBe(false);
216
+ });
217
+
218
+ test("rejects truncated JPEGs and non-JPEG payloads", () => {
219
+ expect(hasValidJpegStructure(minimalJpeg({ dropEoi: true }))).toBe(false);
220
+ expect(
221
+ hasValidJpegStructure(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00])),
222
+ ).toBe(false);
223
+ expect(hasValidJpegStructure(Buffer.alloc(0))).toBe(false);
224
+ expect(hasValidJpegStructure(PNG_1PX_BYTES)).toBe(false);
225
+ });
226
+
227
+ test("rejects a top-level EOI with no frame or scan", () => {
228
+ // Bare SOI+EOI is structurally walkable but carries no image data;
229
+ // providers reject it, so the gate must too.
230
+ expect(hasValidJpegStructure(Buffer.from([0xff, 0xd8, 0xff, 0xd9]))).toBe(
231
+ false,
232
+ );
233
+ // A scan without a frame header is equally undecodable.
234
+ const scanNoFrame = Buffer.concat([
235
+ Buffer.from([0xff, 0xd8]),
236
+ Buffer.from([0xff, 0xda, 0x00, 0x03, 0x01, 0x12]),
237
+ Buffer.from([0xff, 0xd9]),
238
+ ]);
239
+ expect(hasValidJpegStructure(scanNoFrame)).toBe(false);
240
+ });
241
+
242
+ test("rejects a segment length that runs past the buffer", () => {
243
+ // A declared length larger than the remaining bytes is truncation, not a
244
+ // reason to scan beyond the buffer.
245
+ const overrun = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0xff, 0xff, 0x00]);
246
+ expect(hasValidJpegStructure(overrun)).toBe(false);
247
+ });
248
+ });
249
+
134
250
  describe("normalizeImageBytes passthrough", () => {
135
251
  test("non-HEIF bytes pass through untouched", async () => {
136
252
  const result = await normalizeImageBytes("image/png", PNG_1PX_BYTES);
@@ -205,6 +321,9 @@ describe.skipIf(process.platform !== "darwin")(
205
321
  const converted = await convertImageToJpeg(heicBytes);
206
322
  expect(converted).not.toBeNull();
207
323
  expect(startsWithJpegMagic(converted!)).toBe(true);
324
+ // A real encoder's output must pass the structural walk, or the
325
+ // compactor gate would drop every legitimately converted image.
326
+ expect(hasValidJpegStructure(converted!)).toBe(true);
208
327
  });
209
328
 
210
329
  test("conversion options produce distinct outputs (cache key isolation)", async () => {
@@ -228,6 +347,29 @@ describe.skipIf(process.platform !== "darwin")(
228
347
  expect(first!.equals(second!)).toBe(true);
229
348
  });
230
349
 
350
+ test("a poisoned (truncated) cache entry is discarded, not returned", async () => {
351
+ // Distinct quality so this test owns its cache key.
352
+ const quality = 77;
353
+ const cacheDir = join(tmpdir(), "vellum-optimized-images");
354
+ mkdirSync(cacheDir, { recursive: true });
355
+ const poisonedPath = join(
356
+ cacheDir,
357
+ `${cacheKeyFor(heicBytes, quality)}.jpg`,
358
+ );
359
+ // A torn write: valid JPEG head, no EOI tail. readFromCache must
360
+ // discard it rather than return it as the converted image.
361
+ writeFileSync(poisonedPath, Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]));
362
+
363
+ const converted = await convertImageToJpeg(heicBytes, { quality });
364
+ expect(converted).not.toBeNull();
365
+ expect(isCompleteJpeg(converted!)).toBe(true);
366
+
367
+ // The poisoned entry was replaced by the re-converted result.
368
+ expect(existsSync(poisonedPath)).toBe(true);
369
+ const { readFileSync } = await import("node:fs");
370
+ expect(isCompleteJpeg(readFileSync(poisonedPath))).toBe(true);
371
+ });
372
+
231
373
  test("normalizeImageBytes converts to a JPEG master", async () => {
232
374
  const result = await normalizeImageBytes("image/heic", heicBytes);
233
375
  expect(result.converted).toBe(true);
@@ -26,6 +26,7 @@ process.env.GATEWAY_SECURITY_DIR = join(testDir, "protected");
26
26
 
27
27
  import {
28
28
  installIpcMock,
29
+ lastIpcParams,
29
30
  mockIpcResponse,
30
31
  } from "./helpers/gateway-classify-mock.js";
31
32
  installIpcMock();
@@ -33,6 +34,7 @@ mockIpcResponse("classify_risk", {
33
34
  risk: "low",
34
35
  reason: "skill_load",
35
36
  matchType: "unknown",
37
+ scopeOptions: [],
36
38
  });
37
39
  mockIpcResponse("get_global_thresholds", {
38
40
  interactive: "low",
@@ -42,8 +44,7 @@ mockIpcResponse("get_global_thresholds", {
42
44
 
43
45
  // ── Imports (after mocks) ─────────────────────────────────────────────────
44
46
 
45
- import { check, generateAllowlistOptions } from "../permissions/checker.js";
46
- import { clearRiskCache } from "../permissions/checker.js";
47
+ import { check, classifyRisk } from "../permissions/checker.js";
47
48
  import { _clearGlobalCacheForTesting } from "../permissions/gateway-threshold-reader.js";
48
49
  import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
49
50
 
@@ -82,13 +83,34 @@ function writeDynamicSkill(
82
83
  );
83
84
  }
84
85
 
86
+ /**
87
+ * What the gateway skill classifier answers for a load whose params carry
88
+ * `hasInlineExpansions` (gateway/src/risk/skill-risk-classifier.ts): High,
89
+ * registry-matched. The daemon does not elevate locally; the gateway is the
90
+ * one place a dynamic load becomes High.
91
+ */
92
+ function mockDynamicSkillClassification(): void {
93
+ mockIpcResponse("classify_risk", {
94
+ risk: "high",
95
+ reason:
96
+ "Skill load with inline command expansions (executes shell commands at load time)",
97
+ matchType: "registry",
98
+ scopeOptions: [],
99
+ });
100
+ }
101
+
85
102
  // ── Tests ─────────────────────────────────────────────────────────────────
86
103
 
87
104
  describe("inline-command skill_load permissions", () => {
88
105
  beforeEach(() => {
89
- clearRiskCache();
90
106
  _clearGlobalCacheForTesting();
91
107
  setOverridesForTesting({});
108
+ mockIpcResponse("classify_risk", {
109
+ risk: "low",
110
+ reason: "skill_load",
111
+ matchType: "unknown",
112
+ scopeOptions: [],
113
+ });
92
114
  mockIpcResponse("get_global_thresholds", {
93
115
  interactive: "low",
94
116
  autonomous: "medium",
@@ -116,6 +138,7 @@ describe("inline-command skill_load permissions", () => {
116
138
  test("an uncovered dynamic skill prompts below Full access", async () => {
117
139
  ensureSkillsDir();
118
140
  writeDynamicSkill("dynamic-prompt", "Dynamic Prompt Skill");
141
+ mockDynamicSkillClassification();
119
142
 
120
143
  // interactive threshold "low" (beforeEach) is below the High risk of an
121
144
  // inline-command load, so it prompts.
@@ -131,6 +154,7 @@ describe("inline-command skill_load permissions", () => {
131
154
  test("an uncovered dynamic skill runs at Full access (high threshold)", async () => {
132
155
  ensureSkillsDir();
133
156
  writeDynamicSkill("dynamic-full", "Dynamic Full Access Skill");
157
+ mockDynamicSkillClassification();
134
158
  mockIpcResponse("get_global_thresholds", {
135
159
  interactive: "high",
136
160
  autonomous: "high",
@@ -155,6 +179,7 @@ describe("inline-command skill_load permissions", () => {
155
179
  risk: "low",
156
180
  reason: "user rule: skill_load_dynamic:dynamic-covered",
157
181
  matchType: "user_rule",
182
+ scopeOptions: [],
158
183
  });
159
184
 
160
185
  const result = await check(
@@ -163,17 +188,12 @@ describe("inline-command skill_load permissions", () => {
163
188
  "/tmp",
164
189
  );
165
190
  expect(result.decision).toBe("allow");
166
-
167
- mockIpcResponse("classify_risk", {
168
- risk: "low",
169
- reason: "skill_load",
170
- matchType: "unknown",
171
- });
172
191
  });
173
192
 
174
193
  test("dynamic skill prompts in strict mode (no matching rule)", async () => {
175
194
  ensureSkillsDir();
176
195
  writeDynamicSkill("dynamic-strict", "Dynamic Strict Skill");
196
+ mockDynamicSkillClassification();
177
197
  mockIpcResponse("get_global_thresholds", {
178
198
  interactive: "none",
179
199
  autonomous: "none",
@@ -211,43 +231,34 @@ describe("inline-command skill_load permissions", () => {
211
231
 
212
232
  // ── Allowlist options ────────────────────────────────────────────────
213
233
 
214
- describe("allowlist options", () => {
215
- test("dynamic skill allowlist options use skill_load_dynamic: namespace", async () => {
234
+ describe("skill metadata sent to the gateway", () => {
235
+ // The gateway picks the `skill_load_dynamic:` namespace off these flags,
236
+ // so what the daemon reads from disk decides which rule namespace a user
237
+ // can save into.
238
+ test("a skill with inline expansions is sent as dynamic", async () => {
216
239
  ensureSkillsDir();
217
240
  writeDynamicSkill("dynamic-opts", "Dynamic Opts Skill");
218
241
 
219
- const options = await generateAllowlistOptions("skill_load", {
220
- skill: "dynamic-opts",
221
- });
242
+ await classifyRisk("skill_load", { skill: "dynamic-opts" });
222
243
 
223
- expect(options.length).toBeGreaterThanOrEqual(1);
224
- // All options should use skill_load_dynamic: prefix
225
- for (const option of options) {
226
- expect(option.pattern).toMatch(/^skill_load_dynamic:/);
227
- }
228
-
229
- // Should have an any-version option
230
- const anyVersionOption = options.find(
231
- (o) => o.pattern === "skill_load_dynamic:dynamic-opts",
232
- );
233
- expect(anyVersionOption).toBeDefined();
234
- expect(anyVersionOption!.description).toBe("This skill (any version)");
244
+ const metadata = lastIpcParams("classify_risk")?.skillMetadata as
245
+ | { hasInlineExpansions: boolean; isDynamic: boolean }
246
+ | undefined;
247
+ expect(metadata?.hasInlineExpansions).toBe(true);
248
+ expect(metadata?.isDynamic).toBe(true);
235
249
  });
236
250
 
237
- test("plain skill allowlist options use skill_load: namespace", async () => {
251
+ test("a plain skill is not sent as dynamic", async () => {
238
252
  ensureSkillsDir();
239
253
  writePlainSkill("plain-opts", "Plain Opts Skill");
240
254
 
241
- const options = await generateAllowlistOptions("skill_load", {
242
- skill: "plain-opts",
243
- });
255
+ await classifyRisk("skill_load", { skill: "plain-opts" });
244
256
 
245
- expect(options.length).toBeGreaterThanOrEqual(1);
246
- // Should use skill_load: prefix, not skill_load_dynamic:
247
- for (const option of options) {
248
- expect(option.pattern).toMatch(/^skill_load:/);
249
- expect(option.pattern).not.toMatch(/^skill_load_dynamic:/);
250
- }
257
+ const metadata = lastIpcParams("classify_risk")?.skillMetadata as
258
+ | { hasInlineExpansions: boolean; isDynamic: boolean }
259
+ | undefined;
260
+ expect(metadata?.hasInlineExpansions).toBe(false);
261
+ expect(metadata?.isDynamic).toBe(false);
251
262
  });
252
263
  });
253
264
  });
@@ -0,0 +1,207 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, test } from "bun:test";
4
+
5
+ import type { SkillToolEntry } from "../config/skills.js";
6
+ import {
7
+ bundledToolInputAliases,
8
+ bundledToolInputRepairs,
9
+ } from "../tools/shared/input-repairs.js";
10
+
11
+ const TOOL = "scaffold_managed_skill";
12
+ const FIND_TOOL = "find_similar_skills";
13
+
14
+ /** The real manifest entries the repairs describe. */
15
+ function manifestEntry(name: string): SkillToolEntry {
16
+ const manifest = JSON.parse(
17
+ readFileSync(
18
+ join(
19
+ import.meta.dir,
20
+ "../config/bundled-skills/skill-management/TOOLS.json",
21
+ ),
22
+ "utf-8",
23
+ ),
24
+ ) as { tools: SkillToolEntry[] };
25
+ const entry = manifest.tools.find((t) => t.name === name);
26
+ if (!entry) {
27
+ throw new Error(`${name} is missing from the skill-management TOOLS.json`);
28
+ }
29
+ return entry;
30
+ }
31
+
32
+ function declaredProperties(name: string): Record<string, { type?: string }> {
33
+ const schema = manifestEntry(name).input_schema as {
34
+ properties: Record<string, { type?: string }>;
35
+ };
36
+ return schema.properties;
37
+ }
38
+
39
+ describe("bundledToolInputRepairs: parameter aliases", () => {
40
+ test("reads `body` as the declared `body_markdown`", () => {
41
+ const result = bundledToolInputRepairs(TOOL, {
42
+ skill_id: "deploy",
43
+ body: "# Deploy\n",
44
+ });
45
+ expect(result).toEqual({ skill_id: "deploy", body_markdown: "# Deploy\n" });
46
+ expect("body" in result).toBe(false);
47
+ });
48
+
49
+ test("never overwrites a call that spells the parameter correctly", () => {
50
+ const input = { body: "alias", body_markdown: "declared" };
51
+ const result = bundledToolInputRepairs(TOOL, input);
52
+ expect(result).toBe(input);
53
+ });
54
+
55
+ test("leaves a tool with no alias table alone", () => {
56
+ const input = { body: "# Deploy\n" };
57
+ expect(bundledToolInputRepairs("file_write", input)).toBe(input);
58
+ });
59
+
60
+ test.each([TOOL, FIND_TOOL])(
61
+ "%s: no alias shadows a parameter the tool declares",
62
+ (tool) => {
63
+ const declared = declaredProperties(tool);
64
+ const aliases = bundledToolInputAliases(tool);
65
+ expect(aliases.length).toBeGreaterThan(0);
66
+ for (const alias of aliases) {
67
+ expect(declared[alias]).toBeUndefined();
68
+ }
69
+ },
70
+ );
71
+
72
+ test.each([TOOL, FIND_TOOL])(
73
+ "%s: every alias targets a parameter the tool declares",
74
+ (tool) => {
75
+ const declared = declaredProperties(tool);
76
+ for (const alias of bundledToolInputAliases(tool)) {
77
+ const result = bundledToolInputRepairs(tool, { [alias]: "value" });
78
+ const target = Object.keys(result).find((key) => key !== alias);
79
+ expect(target).toBeDefined();
80
+ expect(declared[target!]).toBeDefined();
81
+ }
82
+ },
83
+ );
84
+
85
+ test.each([
86
+ ["title", "name"],
87
+ ["summary", "description"],
88
+ ["content", "body_markdown"],
89
+ ])("reads `%s` as `%s`", (alias, canonical) => {
90
+ const result = bundledToolInputRepairs(TOOL, { [alias]: "value" });
91
+ expect(result).toEqual({ [canonical]: "value" });
92
+ });
93
+
94
+ test.each(["description", "query"])(
95
+ "find_similar_skills reads `%s` as `goal`",
96
+ (alias) => {
97
+ const result = bundledToolInputRepairs(FIND_TOOL, {
98
+ [alias]: "deploy staging",
99
+ });
100
+ expect(result).toEqual({ goal: "deploy staging" });
101
+ },
102
+ );
103
+ });
104
+
105
+ describe("bundledToolInputRepairs: files written as a path-keyed map", () => {
106
+ test("reads string values as each file's contents", () => {
107
+ const result = bundledToolInputRepairs(TOOL, {
108
+ files: {
109
+ "references/failure-modes.md": "The retry loop stalls.",
110
+ "scripts/run.sh": "echo hi",
111
+ },
112
+ });
113
+ expect(result.files).toEqual([
114
+ {
115
+ path: "references/failure-modes.md",
116
+ content: "The retry loop stalls.",
117
+ },
118
+ { path: "scripts/run.sh", content: "echo hi" },
119
+ ]);
120
+ });
121
+
122
+ test("reads an entry that names content or copy_from", () => {
123
+ const result = bundledToolInputRepairs(TOOL, {
124
+ files: {
125
+ "references/notes.md": { content: "hi" },
126
+ "scripts/run.sh": { copy_from: "/tmp/run.sh" },
127
+ },
128
+ });
129
+ expect(result.files).toEqual([
130
+ { path: "references/notes.md", content: "hi" },
131
+ { path: "scripts/run.sh", copy_from: "/tmp/run.sh" },
132
+ ]);
133
+ });
134
+
135
+ test("leaves one entry spelling out its own fields, which is a file not a map", () => {
136
+ const input = { files: { path: "references/notes.md", content: "hi" } };
137
+ expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
138
+ });
139
+
140
+ test("leaves the whole value when any entry is unreadable", () => {
141
+ const input = {
142
+ files: {
143
+ "references/notes.md": "hi",
144
+ "scripts/run.sh": { mode: 755 },
145
+ },
146
+ };
147
+ expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
148
+ });
149
+
150
+ test("leaves a map with a blank path", () => {
151
+ const input = { files: { " ": "hi" } };
152
+ expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
153
+ });
154
+
155
+ test("leaves an empty object", () => {
156
+ const input = { files: {} };
157
+ expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
158
+ });
159
+
160
+ test("an entry spelling out every declared file property is read as one file", () => {
161
+ const schema = manifestEntry(TOOL).input_schema as {
162
+ properties: {
163
+ files: { items: { properties: Record<string, unknown> } };
164
+ };
165
+ };
166
+ const declared = Object.keys(schema.properties.files.items.properties);
167
+ expect(declared).toContain("path");
168
+ const entry = Object.fromEntries(declared.map((key) => [key, "value"]));
169
+
170
+ const input = { files: entry };
171
+ expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
172
+ });
173
+
174
+ test("leaves files that already arrived as an array", () => {
175
+ const input = { files: [{ path: "references/notes.md", content: "hi" }] };
176
+ expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
177
+ });
178
+ });
179
+
180
+ describe("bundledToolInputRepairs: purity", () => {
181
+ test("returns the same object when there is nothing to repair", () => {
182
+ const input = { skill_id: "deploy", body_markdown: "# Deploy\n" };
183
+ expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
184
+ });
185
+
186
+ test("returns a new object and never mutates the original input", () => {
187
+ const input = {
188
+ body: "# Deploy\n",
189
+ files: { "references/notes.md": "hi" },
190
+ };
191
+ const snapshot = JSON.parse(JSON.stringify(input));
192
+ const result = bundledToolInputRepairs(TOOL, input);
193
+ expect(result).not.toBe(input);
194
+ expect(input).toEqual(snapshot);
195
+ });
196
+
197
+ test("repairs an alias and a files map in the same call", () => {
198
+ const result = bundledToolInputRepairs(TOOL, {
199
+ body: "# Deploy\n",
200
+ files: { "references/notes.md": "hi" },
201
+ });
202
+ expect(result).toEqual({
203
+ body_markdown: "# Deploy\n",
204
+ files: [{ path: "references/notes.md", content: "hi" }],
205
+ });
206
+ });
207
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Tests for the live-workspace guard in util/platform.ts: a test process must
3
+ * never resolve the workspace to a directory outside os.tmpdir().
4
+ */
5
+
6
+ import { rmSync, symlinkSync } from "node:fs";
7
+ import { homedir, tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { afterEach, describe, expect, test } from "bun:test";
10
+
11
+ import { getWorkspaceDir } from "../util/platform.js";
12
+
13
+ const originalWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
14
+ const originalAllowReal = process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
15
+
16
+ afterEach(() => {
17
+ process.env.VELLUM_WORKSPACE_DIR = originalWorkspaceDir;
18
+ if (originalAllowReal === undefined) {
19
+ delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
20
+ } else {
21
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = originalAllowReal;
22
+ }
23
+ });
24
+
25
+ describe("live-workspace guard", () => {
26
+ test("allows the preload's tmpdir workspace", () => {
27
+ expect(getWorkspaceDir()).toBe(process.env.VELLUM_WORKSPACE_DIR!);
28
+ });
29
+
30
+ test("allows a not-yet-created nested path under tmpdir", () => {
31
+ const dir = join(
32
+ tmpdir(),
33
+ "vellum-guard-nonexistent",
34
+ "nested",
35
+ "workspace",
36
+ );
37
+ process.env.VELLUM_WORKSPACE_DIR = dir;
38
+ expect(getWorkspaceDir()).toBe(dir);
39
+ });
40
+
41
+ test("refuses a non-tmpdir workspace in a test process", () => {
42
+ delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
43
+ process.env.VELLUM_WORKSPACE_DIR = join(homedir(), "vellum-guard-live");
44
+ expect(() => getWorkspaceDir()).toThrow(/Refusing to use/);
45
+ });
46
+
47
+ test("refuses a tmpdir symlink that resolves outside tmpdir", () => {
48
+ delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
49
+ const link = join(tmpdir(), `vellum-guard-escape-${process.pid}`);
50
+ rmSync(link, { force: true });
51
+ symlinkSync(homedir(), link);
52
+ try {
53
+ process.env.VELLUM_WORKSPACE_DIR = link;
54
+ expect(() => getWorkspaceDir()).toThrow(/Refusing to use/);
55
+ } finally {
56
+ rmSync(link, { force: true });
57
+ }
58
+ });
59
+
60
+ test("VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS=1 bypasses the guard", () => {
61
+ const dir = join(homedir(), "vellum-guard-live-optout");
62
+ process.env.VELLUM_WORKSPACE_DIR = dir;
63
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
64
+ expect(getWorkspaceDir()).toBe(dir);
65
+ });
66
+
67
+ test("opt-out is per-call, not cached: same dir throws once the var clears", () => {
68
+ const dir = join(homedir(), "vellum-guard-live-optout-cleared");
69
+ process.env.VELLUM_WORKSPACE_DIR = dir;
70
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
71
+ expect(getWorkspaceDir()).toBe(dir);
72
+ delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
73
+ expect(() => getWorkspaceDir()).toThrow(/Refusing to use/);
74
+ });
75
+ });
@@ -12,7 +12,7 @@ mock.module("../config/env-registry.js", () => ({
12
12
  getIsContainerized: () => false,
13
13
  getIsPlatform: () => false,
14
14
  isPlatformRemote: () => false,
15
- getWorkspaceDirOverride: () => undefined,
15
+ getWorkspaceDirOverride: () => process.env.VELLUM_WORKSPACE_DIR,
16
16
  getBackupDirOverride: () => undefined,
17
17
  getBackupKeyPathOverride: () => undefined,
18
18
  getCpuLimit: () => undefined,
@@ -12,7 +12,7 @@ mock.module("../config/env-registry.js", () => ({
12
12
  getIsContainerized: () => false,
13
13
  getIsPlatform: () => false,
14
14
  isPlatformRemote: () => false,
15
- getWorkspaceDirOverride: () => undefined,
15
+ getWorkspaceDirOverride: () => process.env.VELLUM_WORKSPACE_DIR,
16
16
  getBackupDirOverride: () => undefined,
17
17
  getBackupKeyPathOverride: () => undefined,
18
18
  getCpuLimit: () => undefined,
@@ -12,7 +12,7 @@ mock.module("../config/env-registry.js", () => ({
12
12
  getIsContainerized: () => false,
13
13
  getIsPlatform: () => false,
14
14
  isPlatformRemote: () => false,
15
- getWorkspaceDirOverride: () => undefined,
15
+ getWorkspaceDirOverride: () => process.env.VELLUM_WORKSPACE_DIR,
16
16
  getBackupDirOverride: () => undefined,
17
17
  getBackupKeyPathOverride: () => undefined,
18
18
  getCpuLimit: () => undefined,