@vellumai/assistant 0.11.4 → 0.11.5-dev.202608220019.a778cf0

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 (457) hide show
  1. package/AGENTS.md +6 -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__/ingress.test.ts +118 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  15. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  19. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  20. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  21. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  22. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +30 -32
  23. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  24. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  25. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  26. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  28. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  29. package/openapi.yaml +487 -11
  30. package/package.json +1 -1
  31. package/scripts/sync-web-search-catalog.ts +6 -0
  32. package/src/__tests__/anthropic-provider.test.ts +10 -3
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  34. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  35. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  36. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  37. package/src/__tests__/channel-readiness-service.test.ts +126 -0
  38. package/src/__tests__/channel-reply-delivery.test.ts +30 -17
  39. package/src/__tests__/checker.test.ts +20 -314
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  41. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  42. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  44. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  45. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  46. package/src/__tests__/conversation-attachments.test.ts +0 -1
  47. package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
  48. package/src/__tests__/conversation-error.test.ts +17 -0
  49. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  50. package/src/__tests__/conversation-pairing.test.ts +186 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  52. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  53. package/src/__tests__/credential-routes.test.ts +22 -3
  54. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  55. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  56. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  57. package/src/__tests__/dm-backfill.test.ts +63 -0
  58. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  59. package/src/__tests__/edit-propagation.test.ts +107 -4
  60. package/src/__tests__/events-client-registration.test.ts +28 -0
  61. package/src/__tests__/file-write-tool.test.ts +4 -2
  62. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  63. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  64. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  65. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  66. package/src/__tests__/host-shell-tool.test.ts +23 -4
  67. package/src/__tests__/image-conversion.test.ts +143 -1
  68. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  69. package/src/__tests__/input-repairs.test.ts +207 -0
  70. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  71. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  72. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  73. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  74. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  75. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  76. package/src/__tests__/mtime-cache.test.ts +2 -0
  77. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  78. package/src/__tests__/openai-provider.test.ts +40 -0
  79. package/src/__tests__/openai-responses-provider.test.ts +18 -0
  80. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  81. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  82. package/src/__tests__/platform.test.ts +16 -1
  83. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  84. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  85. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  86. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  87. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  88. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  89. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  90. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  91. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  92. package/src/__tests__/reaction-persistence.test.ts +200 -7
  93. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  94. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  95. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  96. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  97. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  98. package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
  99. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  100. package/src/__tests__/settings-routes.test.ts +85 -3
  101. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  102. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  103. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  104. package/src/__tests__/tool-executor.test.ts +63 -38
  105. package/src/__tests__/tool-policy.test.ts +97 -1
  106. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  107. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  108. package/src/__tests__/validate-input.test.ts +243 -7
  109. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  110. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  111. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  112. package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
  113. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  114. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  115. package/src/acp/acp-claude-oauth.ts +38 -21
  116. package/src/acp/prepare-agent-env.ts +146 -55
  117. package/src/agent/loop-tool-dedup.test.ts +161 -0
  118. package/src/agent/loop.ts +68 -22
  119. package/src/api/constants/app-tools.ts +34 -0
  120. package/src/api/constants/profile-config-validation.ts +60 -0
  121. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  122. package/src/api/events/tool-result.ts +6 -1
  123. package/src/api/events/watch-retro-completed.ts +52 -0
  124. package/src/api/index.ts +30 -0
  125. package/src/api/responses/resource-pressure-status.ts +25 -0
  126. package/src/approvals/guardian-channel-delivery.ts +70 -6
  127. package/src/approvals/guardian-request-resolvers.ts +32 -66
  128. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  129. package/src/calls/voice-session-bridge.ts +60 -6
  130. package/src/channels/__tests__/message-audience.test.ts +49 -0
  131. package/src/channels/__tests__/types.test.ts +17 -3
  132. package/src/channels/message-audience.ts +40 -0
  133. package/src/channels/types.ts +16 -12
  134. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  135. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  136. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  137. package/src/cli/commands/__tests__/conversations-slack.test.ts +2 -1
  138. package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
  139. package/src/cli/commands/__tests__/inference-providers.test.ts +135 -2
  140. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  141. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  142. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  143. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  144. package/src/cli/commands/conversations.help.ts +34 -0
  145. package/src/cli/commands/conversations.ts +125 -0
  146. package/src/cli/commands/inference-profiles.ts +56 -3
  147. package/src/cli/commands/inference-providers.ts +37 -6
  148. package/src/cli/commands/inference.help.ts +9 -4
  149. package/src/cli/commands/keys.help.ts +13 -1
  150. package/src/cli/commands/keys.ts +26 -15
  151. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  152. package/src/cli/commands/memory/index.help.ts +41 -27
  153. package/src/cli/commands/memory/index.ts +2 -0
  154. package/src/cli/commands/memory/memory-v2.ts +58 -54
  155. package/src/cli/commands/memory/memory-validate.ts +18 -0
  156. package/src/cli/commands/monitoring.ts +1 -0
  157. package/src/cli/commands/oauth/request.test.ts +93 -0
  158. package/src/cli/commands/oauth/request.ts +23 -9
  159. package/src/cli/commands/plugins.help.ts +28 -31
  160. package/src/cli/commands/plugins.ts +10 -0
  161. package/src/cli/commands/skills.help.ts +13 -16
  162. package/src/cli/commands/trust.ts +3 -14
  163. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  164. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  165. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  166. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  167. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  168. package/src/cli/lib/bundled-marketplace.json +26 -0
  169. package/src/cli/lib/daemon-credential-client.ts +25 -3
  170. package/src/cli/lib/install-from-github.ts +35 -24
  171. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  172. package/src/cli/lib/open-browser.test.ts +67 -0
  173. package/src/cli/lib/open-browser.ts +24 -5
  174. package/src/cli/lib/plugin-surfaces.ts +27 -0
  175. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  176. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  177. package/src/config/__tests__/profile-materialization.test.ts +26 -0
  178. package/src/config/default-profile-catalog.ts +5 -6
  179. package/src/config/feature-flag-registry.json +36 -28
  180. package/src/config/profile-materialization.ts +29 -0
  181. package/src/config/sanitize-for-transfer.ts +16 -0
  182. package/src/config/schemas/llm.ts +7 -0
  183. package/src/config/schemas/services.ts +6 -0
  184. package/src/config/webhook-routing.ts +8 -0
  185. package/src/context/compactor.ts +31 -2
  186. package/src/context/outbound-sanitize.ts +6 -0
  187. package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
  188. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  189. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  190. package/src/daemon/conversation-error.ts +19 -2
  191. package/src/daemon/conversation-messaging.ts +22 -2
  192. package/src/daemon/conversation-process.ts +11 -0
  193. package/src/daemon/conversation-store.ts +10 -0
  194. package/src/daemon/conversation-surfaces.ts +21 -8
  195. package/src/daemon/conversation.ts +12 -10
  196. package/src/daemon/daemon-control.ts +1 -0
  197. package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
  198. package/src/daemon/handlers/config-ingress.ts +115 -5
  199. package/src/daemon/lifecycle.ts +30 -2
  200. package/src/daemon/message-types/conversations.ts +5 -7
  201. package/src/daemon/message-types/web-activity.ts +3 -2
  202. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  203. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  204. package/src/daemon/resource-pressure-guard.ts +387 -0
  205. package/src/daemon/shutdown-handlers.ts +2 -0
  206. package/src/daemon/startup-error.ts +16 -0
  207. package/src/daemon/trust-context.ts +14 -10
  208. package/src/daemon/unsendable-image-notice.ts +76 -0
  209. package/src/hooks/registry.ts +65 -51
  210. package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
  211. package/src/inbound/tunnel-probe.ts +261 -0
  212. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  213. package/src/ipc/gateway-client.test.ts +1 -1
  214. package/src/ipc/gateway-client.ts +14 -21
  215. package/src/ipc/socket-cleanup.ts +2 -11
  216. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  217. package/src/messaging/provider-message-metadata.ts +128 -0
  218. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +138 -73
  219. package/src/messaging/providers/channel-transport.ts +100 -16
  220. package/src/messaging/providers/discord/send.ts +16 -0
  221. package/src/messaging/providers/discord/transport.ts +23 -1
  222. package/src/messaging/providers/index.ts +84 -16
  223. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  224. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  225. package/src/messaging/providers/slack/send.test.ts +88 -34
  226. package/src/messaging/providers/slack/send.ts +99 -50
  227. package/src/messaging/providers/slack/transport.ts +35 -37
  228. package/src/messaging/providers/telegram-bot/send.test.ts +88 -18
  229. package/src/messaging/providers/telegram-bot/send.ts +36 -0
  230. package/src/messaging/providers/telegram-bot/transport.ts +22 -8
  231. package/src/messaging/read-provider-metadata.test.ts +157 -0
  232. package/src/messaging/read-provider-metadata.ts +51 -0
  233. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  234. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  235. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  236. package/src/notifications/adapters/macos.ts +55 -0
  237. package/src/notifications/adapters/slack.ts +10 -5
  238. package/src/notifications/assistant-reply-producer.ts +37 -4
  239. package/src/notifications/conversation-pairing.ts +111 -21
  240. package/src/notifications/edit-notification.ts +38 -8
  241. package/src/notifications/emit-signal.ts +12 -14
  242. package/src/notifications/home-feed-side-effect.ts +218 -15
  243. package/src/notifications/types.ts +5 -0
  244. package/src/permissions/AGENTS.md +16 -0
  245. package/src/permissions/checker.test.ts +75 -122
  246. package/src/permissions/checker.ts +44 -560
  247. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  248. package/src/permissions/confirmation-guardian-request.ts +5 -1
  249. package/src/persistence/conversation-crud.ts +69 -20
  250. package/src/persistence/conversation-queries.ts +14 -1
  251. package/src/persistence/conversation-types.ts +22 -0
  252. package/src/persistence/db-async-query.ts +2 -1
  253. package/src/persistence/db-maintenance.ts +15 -0
  254. package/src/persistence/delivery-crud.ts +122 -15
  255. package/src/persistence/embeddings/qdrant-manager.ts +1 -0
  256. package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
  257. package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
  258. package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
  259. package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
  260. package/src/persistence/schema/index.ts +1 -0
  261. package/src/persistence/schema/watch.ts +29 -0
  262. package/src/persistence/steps.ts +4 -0
  263. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  264. package/src/plugin-api/conversation-turn.ts +27 -0
  265. package/src/plugin-api/credential-scope.test.ts +15 -0
  266. package/src/plugin-api/credential-scope.ts +14 -0
  267. package/src/plugin-api/index.ts +19 -1
  268. package/src/plugin-api/resolve-credential.ts +15 -10
  269. package/src/plugin-api/store-credential.ts +148 -0
  270. package/src/plugin-api/system-card.ts +36 -0
  271. package/src/plugin-api/vision-support.test.ts +82 -0
  272. package/src/plugin-api/vision-support.ts +14 -2
  273. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  274. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  275. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  276. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  277. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  278. package/src/plugins/defaults/main.ts +15 -0
  279. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  280. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  281. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  282. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  283. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  284. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  286. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  287. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  288. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  289. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  290. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  291. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  292. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  293. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  294. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  295. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  296. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  297. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  298. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  299. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  300. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  301. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  302. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  303. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  304. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  305. package/src/plugins/mtime-cache.ts +1 -1
  306. package/src/plugins/pipeline.ts +14 -7
  307. package/src/plugins/plugin-execution-context.ts +35 -11
  308. package/src/plugins/plugin-tree-walk.ts +63 -4
  309. package/src/providers/__tests__/retry-network-error.test.ts +84 -0
  310. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  311. package/src/providers/anthropic/client.ts +332 -241
  312. package/src/providers/connection-resolution.ts +25 -2
  313. package/src/providers/content-blocks.ts +22 -0
  314. package/src/providers/fetch-provider-catalog.ts +19 -0
  315. package/src/providers/gemini/client.ts +5 -2
  316. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  317. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  318. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  319. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  320. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  321. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  322. package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
  323. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  324. package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
  325. package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
  326. package/src/providers/inference/adapter-factory.ts +33 -2
  327. package/src/providers/inference/auth.ts +13 -2
  328. package/src/providers/inference/credential-usage.ts +37 -0
  329. package/src/providers/inference/endpoint-probe.ts +115 -0
  330. package/src/providers/inference/missing-credential-guard.ts +110 -0
  331. package/src/providers/inference/profile-probe.ts +256 -0
  332. package/src/providers/inference/resolve-auth.ts +7 -7
  333. package/src/providers/media-resolve.ts +176 -15
  334. package/src/providers/model-catalog.ts +10 -0
  335. package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
  336. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  337. package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
  338. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  339. package/src/providers/openai/api-error-normalization.ts +16 -2
  340. package/src/providers/openai/chat-completions-provider.ts +326 -44
  341. package/src/providers/openai/responses-provider.ts +5 -2
  342. package/src/providers/provider-send-message.ts +11 -0
  343. package/src/providers/retry.ts +6 -0
  344. package/src/providers/search-provider-catalog.ts +20 -0
  345. package/src/routes/route-host-protocol.ts +7 -0
  346. package/src/routes/worker.ts +31 -10
  347. package/src/runtime/AGENTS.md +36 -0
  348. package/src/runtime/__tests__/host-observe.test.ts +302 -0
  349. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  350. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  351. package/src/runtime/assistant-event-hub.ts +38 -0
  352. package/src/runtime/channel-readiness-service.ts +39 -13
  353. package/src/runtime/channel-readiness-types.ts +49 -2
  354. package/src/runtime/channel-reply-delivery.ts +55 -32
  355. package/src/runtime/channel-retry-sweep.ts +6 -4
  356. package/src/runtime/effective-capabilities.test.ts +19 -0
  357. package/src/runtime/effective-capabilities.ts +25 -0
  358. package/src/runtime/guardian-reply-router.ts +6 -2
  359. package/src/runtime/host-observe.ts +246 -0
  360. package/src/runtime/http-errors.ts +1 -0
  361. package/src/runtime/http-server.ts +185 -6
  362. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  363. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  364. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
  365. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  366. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
  367. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +72 -1
  368. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
  369. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  370. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  371. package/src/runtime/routes/__tests__/plugins-routes.test.ts +103 -71
  372. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  373. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  374. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  375. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  376. package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
  377. package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
  378. package/src/runtime/routes/approval-routes.ts +37 -4
  379. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  380. package/src/runtime/routes/canned-message-complete.ts +68 -29
  381. package/src/runtime/routes/channel-readiness-routes.ts +14 -2
  382. package/src/runtime/routes/client-routes.ts +113 -0
  383. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  384. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  385. package/src/runtime/routes/conversation-query-routes.ts +10 -0
  386. package/src/runtime/routes/conversation-routes.ts +36 -22
  387. package/src/runtime/routes/credential-in-use.ts +85 -0
  388. package/src/runtime/routes/credential-routes.ts +55 -85
  389. package/src/runtime/routes/guardian-approval-interception.ts +44 -53
  390. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  391. package/src/runtime/routes/host-cu-routes.ts +18 -0
  392. package/src/runtime/routes/identity-routes.ts +5 -121
  393. package/src/runtime/routes/inbound-message-handler.ts +74 -30
  394. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  395. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +182 -297
  396. package/src/runtime/routes/inbound-stages/background-dispatch.ts +173 -386
  397. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  398. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  399. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  400. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  401. package/src/runtime/routes/index.ts +4 -0
  402. package/src/runtime/routes/inference-profiles-routes.ts +232 -31
  403. package/src/runtime/routes/inference-provider-connection-routes.ts +33 -11
  404. package/src/runtime/routes/ingress-status-routes.ts +180 -0
  405. package/src/runtime/routes/monitoring-routes.ts +9 -1
  406. package/src/runtime/routes/oauth-commands-routes.ts +1 -1
  407. package/src/runtime/routes/plugins-routes.ts +28 -32
  408. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  409. package/src/runtime/routes/secret-routes.ts +39 -8
  410. package/src/runtime/routes/settings-routes.ts +15 -12
  411. package/src/runtime/routes/surface-action-routes.ts +20 -19
  412. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  413. package/src/runtime/routes/user-route-resolution.ts +18 -1
  414. package/src/runtime/routes/watch-routes.ts +807 -0
  415. package/src/runtime/slack-reply-session.test.ts +23 -13
  416. package/src/runtime/slack-reply-session.ts +38 -55
  417. package/src/runtime/web-presence.ts +90 -0
  418. package/src/security/__tests__/untrusted-content.test.ts +42 -0
  419. package/src/security/untrusted-content.ts +28 -9
  420. package/src/skills/validate-input.ts +177 -40
  421. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  422. package/src/subagent/consult-context.ts +7 -23
  423. package/src/tools/credentials/broker.ts +24 -79
  424. package/src/tools/credentials/ref-parse.ts +35 -0
  425. package/src/tools/credentials/resolve.ts +4 -10
  426. package/src/tools/credentials/store.ts +168 -0
  427. package/src/tools/credentials/tool-policy.ts +67 -0
  428. package/src/tools/executor.ts +53 -15
  429. package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
  430. package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
  431. package/src/tools/network/__tests__/web-search.test.ts +138 -3
  432. package/src/tools/network/firecrawl-compat.ts +90 -0
  433. package/src/tools/network/url-safety.ts +9 -16
  434. package/src/tools/network/web-fetch.ts +142 -62
  435. package/src/tools/network/web-search.ts +162 -58
  436. package/src/tools/permission-checker.ts +43 -52
  437. package/src/tools/schema-transforms.ts +40 -5
  438. package/src/tools/shared/input-repairs.ts +160 -0
  439. package/src/tools/skills/skill-tool-factory.ts +18 -4
  440. package/src/tools/subagent/spawn.ts +0 -1
  441. package/src/tools/tool-approval-handler.ts +16 -10
  442. package/src/tools/tool-types.ts +26 -13
  443. package/src/tools/types.ts +6 -6
  444. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  445. package/src/util/cgroup-memory.ts +3 -0
  446. package/src/util/container-cpu-sampler.ts +250 -0
  447. package/src/util/image-conversion.ts +178 -14
  448. package/src/util/platform.ts +93 -10
  449. package/src/util/worker-process.ts +1 -0
  450. package/src/watch/__tests__/watch-retro.test.ts +665 -0
  451. package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
  452. package/src/watch/__tests__/watch-timeline.test.ts +670 -0
  453. package/src/watch/watch-retro.ts +480 -0
  454. package/src/watch/watch-session-manager.ts +575 -0
  455. package/src/watch/watch-timeline.ts +848 -0
  456. package/src/permissions/ipc-risk-types.ts +0 -143
  457. package/src/permissions/risk-types.ts +0 -76
@@ -0,0 +1,135 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ canonicalizeWebUrl,
5
+ looksLikeHostPortShorthand,
6
+ looksLikePathOnlyInput,
7
+ normalizeWebUrl,
8
+ } from "../url-normalization.js";
9
+
10
+ /**
11
+ * These are the rules a trust rule is saved and matched under. A change here
12
+ * changes which saved rules still match, so each case states the target the
13
+ * two spellings must agree on.
14
+ */
15
+ describe("normalizeWebUrl", () => {
16
+ test("keeps an ordinary https URL as-is", () => {
17
+ expect(normalizeWebUrl("https://example.com/docs/page")?.href).toBe(
18
+ "https://example.com/docs/page",
19
+ );
20
+ });
21
+
22
+ test("drops the fragment: it never reaches the server", () => {
23
+ expect(normalizeWebUrl("https://example.com/docs#section")?.href).toBe(
24
+ "https://example.com/docs",
25
+ );
26
+ });
27
+
28
+ test("drops userinfo so credentials cannot land in a saved rule", () => {
29
+ const credentialed = new URL("https://example.com/docs/page");
30
+ credentialed.username = "demo";
31
+ credentialed.password = ["c", "r", "e", "d", "1", "2", "3"].join("");
32
+
33
+ const normalized = normalizeWebUrl(credentialed.href);
34
+ expect(normalized?.href).toBe("https://example.com/docs/page");
35
+ expect(normalized?.username).toBe("");
36
+ expect(normalized?.password).toBe("");
37
+ });
38
+
39
+ test("strips a trailing root dot from the hostname", () => {
40
+ expect(normalizeWebUrl("https://example.com./docs/page")?.href).toBe(
41
+ "https://example.com/docs/page",
42
+ );
43
+ });
44
+
45
+ test("decodes escaped path segments so one path has one spelling", () => {
46
+ // Without this, a rule scoped to /private is bypassed by /%70rivate.
47
+ expect(normalizeWebUrl("https://example.com/%70rivate")?.href).toBe(
48
+ "https://example.com/private",
49
+ );
50
+ });
51
+
52
+ test("reads scheme-less input as https", () => {
53
+ expect(normalizeWebUrl("example.com/docs")?.href).toBe(
54
+ "https://example.com/docs",
55
+ );
56
+ });
57
+
58
+ test("reads host:port shorthand as an https origin, not a scheme", () => {
59
+ expect(normalizeWebUrl("example.com:8443/status")?.origin).toBe(
60
+ "https://example.com:8443",
61
+ );
62
+ expect(normalizeWebUrl("[2001:db8::1]:8443/status")?.origin).toBe(
63
+ "https://[2001:db8::1]:8443",
64
+ );
65
+ });
66
+
67
+ test("rejects path-only input rather than coercing it to a host", () => {
68
+ for (const input of ["/etc/passwd", "./rel", "../up", "?q=1", "#frag"]) {
69
+ expect(normalizeWebUrl(input)).toBeNull();
70
+ }
71
+ });
72
+
73
+ test("rejects every non-http scheme", () => {
74
+ for (const input of [
75
+ "file:///etc/passwd",
76
+ "data:text/html,<script>",
77
+ "javascript:alert(1)",
78
+ "ftp://example.com/f",
79
+ ]) {
80
+ expect(normalizeWebUrl(input)).toBeNull();
81
+ }
82
+ });
83
+
84
+ test("rejects empty and whitespace-only input", () => {
85
+ expect(normalizeWebUrl("")).toBeNull();
86
+ expect(normalizeWebUrl(" ")).toBeNull();
87
+ });
88
+
89
+ test("trims surrounding whitespace", () => {
90
+ expect(normalizeWebUrl(" https://example.com/a ")?.href).toBe(
91
+ "https://example.com/a",
92
+ );
93
+ });
94
+
95
+ test("returns null rather than throwing on an unparseable authority", () => {
96
+ expect(normalizeWebUrl("https://")).toBeNull();
97
+ });
98
+ });
99
+
100
+ describe("canonicalizeWebUrl", () => {
101
+ test("keeps the parser's form when the path is not decodable, without throwing", () => {
102
+ // `%zz` and a truncated UTF-8 sequence are not valid escapes; the path
103
+ // must survive unchanged rather than the call throwing.
104
+ expect(canonicalizeWebUrl(new URL("https://example.com/100%zz")).href).toBe(
105
+ "https://example.com/100%zz",
106
+ );
107
+ expect(
108
+ canonicalizeWebUrl(new URL("https://example.com/%E0%A4%A")).href,
109
+ ).toBe("https://example.com/%E0%A4%A");
110
+ });
111
+
112
+ test("decodes an escaped percent, so `%25` and `%` are one path", () => {
113
+ expect(canonicalizeWebUrl(new URL("https://example.com/100%25")).href).toBe(
114
+ "https://example.com/100%",
115
+ );
116
+ });
117
+ });
118
+
119
+ describe("input shape predicates", () => {
120
+ test("host:port shorthand is recognized, scheme-prefixed input is not", () => {
121
+ expect(looksLikeHostPortShorthand("example.com:8443/x")).toBe(true);
122
+ expect(looksLikeHostPortShorthand("[2001:db8::1]:443")).toBe(true);
123
+ expect(looksLikeHostPortShorthand("https://example.com/x")).toBe(false);
124
+ expect(looksLikeHostPortShorthand("example.com/x")).toBe(false);
125
+ });
126
+
127
+ test("path-only input is recognized", () => {
128
+ expect(looksLikePathOnlyInput("/abs")).toBe(true);
129
+ expect(looksLikePathOnlyInput("./rel")).toBe(true);
130
+ expect(looksLikePathOnlyInput("../up")).toBe(true);
131
+ expect(looksLikePathOnlyInput("?q=1")).toBe(true);
132
+ expect(looksLikePathOnlyInput("#frag")).toBe(true);
133
+ expect(looksLikePathOnlyInput("example.com/x")).toBe(false);
134
+ });
135
+ });
@@ -29,3 +29,4 @@ export * from "./trust-rules.js";
29
29
  export * from "./ingress.js";
30
30
  export * from "./remote-web-pairing.js";
31
31
  export * from "./twilio-ingress.js";
32
+ export * from "./url-normalization.js";
@@ -48,3 +48,106 @@ export function normalizeHttpPublicBaseUrl(value: unknown): string | undefined {
48
48
  return undefined;
49
49
  }
50
50
  }
51
+
52
+ /**
53
+ * The same validation as `normalizeHttpPublicBaseUrl`, minus the root path it
54
+ * always emits (`https://x` becomes `https://x/`). Callers that append a path
55
+ * or persist the result want this shape, so they neither join through a double
56
+ * slash nor re-strip what the normalizer just added.
57
+ */
58
+ export function normalizeHttpPublicBaseUrlWithoutTrailingSlash(
59
+ value: unknown,
60
+ ): string | undefined {
61
+ return normalizePublicBaseUrl(normalizeHttpPublicBaseUrl(value));
62
+ }
63
+
64
+ /**
65
+ * A trimmed string, or undefined when the value is not a non-blank string.
66
+ *
67
+ * Exported because the identity check in the daemon's tunnel probe compares an
68
+ * id recorded here against one an edge serves, and the two sides have to trim
69
+ * alike: a padded served id must read as a match, not as a different
70
+ * assistant.
71
+ */
72
+ export function trimmedNonEmptyString(value: unknown): string | undefined {
73
+ if (typeof value !== "string") {
74
+ return undefined;
75
+ }
76
+ const trimmed = value.trim();
77
+ return trimmed.length > 0 ? trimmed : undefined;
78
+ }
79
+
80
+ // Records `vellum tunnel` leaves in the workspace config's `ingress` section.
81
+ // The CLI writes them and the daemon reads them, and the two are separate
82
+ // build units that cannot import each other, so the key names, the provider
83
+ // allowlist, and the validation all live here rather than in a copy on each
84
+ // side.
85
+
86
+ /** Key under `ingress` holding the tunnel that most recently ran. */
87
+ export const INGRESS_LAST_TUNNEL_KEY = "lastTunnel";
88
+
89
+ /**
90
+ * Key under `ingress` holding a tunnel published for device pairing alone.
91
+ *
92
+ * A tailnet-only tunnel started while webhook integrations are configured
93
+ * cannot carry their callbacks, so it leaves `publicBaseUrl` (the callback
94
+ * base) as it is and records itself here. Readers that answer the pairing card
95
+ * prefer this address over `publicBaseUrl`; teardown drops the key.
96
+ */
97
+ export const INGRESS_PAIRING_TUNNEL_KEY = "pairingTunnel";
98
+
99
+ /** Key under `ingress` holding the assistant id that tunnel fronted. */
100
+ export const INGRESS_ASSISTANT_ID_KEY = "assistantId";
101
+
102
+ /**
103
+ * The local tunnel providers the CLI can start, in the order they are offered.
104
+ * `vellum tunnel`'s accepted `--provider` values and the persisted-record
105
+ * validation below both derive from it.
106
+ */
107
+ export const TUNNEL_PROVIDERS = ["ngrok", "cloudflare", "tailscale"] as const;
108
+
109
+ export type TunnelProviderName = (typeof TUNNEL_PROVIDERS)[number];
110
+
111
+ /** A recorded tunnel, kept after teardown so UIs can name the command to restart. */
112
+ export interface TunnelRecord {
113
+ provider: TunnelProviderName;
114
+ publicBaseUrl: string;
115
+ }
116
+
117
+ function isTunnelProviderName(value: unknown): value is TunnelProviderName {
118
+ return (
119
+ typeof value === "string" &&
120
+ (TUNNEL_PROVIDERS as readonly string[]).includes(value)
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Parse an `ingress.lastTunnel` or `ingress.pairingTunnel` value, or null when
126
+ * it is absent or malformed.
127
+ *
128
+ * A hand-edited config must not hand callers an unusable address or a provider
129
+ * name no command accepts: the provider is checked against the allowlist
130
+ * because readers render it into a restart command, and the URL faces the same
131
+ * absolute HTTP(S) constraint as every other public base URL. The URL comes
132
+ * back in the shape the validator produced, so readers never re-normalize it.
133
+ */
134
+ export function parseTunnelRecord(value: unknown): TunnelRecord | null {
135
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
136
+ return null;
137
+ }
138
+ const { provider, publicBaseUrl } = value as Record<string, unknown>;
139
+ if (!isTunnelProviderName(provider)) {
140
+ return null;
141
+ }
142
+ const normalized =
143
+ normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
144
+ if (normalized === undefined) {
145
+ return null;
146
+ }
147
+ return { provider, publicBaseUrl: normalized };
148
+ }
149
+
150
+ /** Parse an `ingress.assistantId` value, or null when it is absent or blank. */
151
+ export function parseRecordedAssistantId(value: unknown): string | null {
152
+ return trimmedNonEmptyString(value) ?? null;
153
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Canonical URL normalization for web tool inputs, shared by the gateway's
3
+ * risk classifiers and the daemon's URL safety checks.
4
+ *
5
+ * A URL a model passes to `web_fetch` / `network_request` reaches more than
6
+ * one consumer, and they must agree on what counts as the same target: the
7
+ * gateway builds the trust-rule ladder from it, and anything that later
8
+ * compares a URL to a saved rule has to fold the same spellings together. Two
9
+ * normalizations would mean two answers, so it lives here rather than in
10
+ * either process.
11
+ *
12
+ * Data-only in spirit: no imports, no config, no I/O.
13
+ */
14
+
15
+ /** Whether a bare `host:port` (or `[v6]:port`) shorthand was written. */
16
+ export function looksLikeHostPortShorthand(value: string): boolean {
17
+ if (/^\[[0-9a-fA-F:.%]+\]:\d+(?:[/?#]|$)/.test(value)) {
18
+ return true;
19
+ }
20
+ return /^[^/?#@\s:]+:\d+(?:[/?#]|$)/.test(value);
21
+ }
22
+
23
+ /** Whether the input is a path, query, or fragment rather than a URL. */
24
+ export function looksLikePathOnlyInput(value: string): boolean {
25
+ return (
26
+ value.startsWith("/") ||
27
+ value.startsWith("./") ||
28
+ value.startsWith("../") ||
29
+ value.startsWith("?") ||
30
+ value.startsWith("#")
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Strip the parts of a URL that must not affect a trust decision, and fold
36
+ * the encodings that would otherwise let one URL wear two spellings.
37
+ *
38
+ * Percent-escaped path segments are decoded (`/%70rivate` and `/private` are
39
+ * one path, so a path-scoped rule cannot be bypassed by escaping), a trailing
40
+ * root dot is dropped from the hostname, and fragment and userinfo are
41
+ * removed: the fragment never reaches the server, and credentials in the
42
+ * authority must never end up in a saved rule.
43
+ */
44
+ export function canonicalizeWebUrl(parsed: URL): URL {
45
+ parsed.hash = "";
46
+ parsed.username = "";
47
+ parsed.password = "";
48
+
49
+ try {
50
+ parsed.pathname = decodeURI(parsed.pathname);
51
+ } catch {
52
+ // Keep the URL parser's canonical form when decoding fails.
53
+ }
54
+
55
+ if (parsed.hostname.endsWith(".")) {
56
+ parsed.hostname = parsed.hostname.replace(/\.+$/, "");
57
+ }
58
+
59
+ return parsed;
60
+ }
61
+
62
+ /**
63
+ * Parse and canonicalize a web tool's `url` input, or `null` when it is not a
64
+ * URL this system will fetch.
65
+ *
66
+ * Accepts `https://host/path`, `http://…`, bare `host/path`, and `host:port`
67
+ * shorthand. Rejects path-only input and any other scheme (`file:`, `data:`,
68
+ * `javascript:`), so a non-http target can never acquire a web trust rule.
69
+ */
70
+ export function normalizeWebUrl(rawUrl: string): URL | null {
71
+ const trimmed = rawUrl.trim();
72
+ if (!trimmed) {
73
+ return null;
74
+ }
75
+
76
+ if (looksLikeHostPortShorthand(trimmed)) {
77
+ try {
78
+ return canonicalizeWebUrl(new URL(`https://${trimmed}`));
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ try {
85
+ const parsed = new URL(trimmed);
86
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") {
87
+ return canonicalizeWebUrl(parsed);
88
+ }
89
+ return null;
90
+ } catch {
91
+ // Not an absolute URL; fall through to the shorthand forms.
92
+ }
93
+
94
+ if (looksLikePathOnlyInput(trimmed)) {
95
+ return null;
96
+ }
97
+
98
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
99
+ return null;
100
+ }
101
+
102
+ try {
103
+ return canonicalizeWebUrl(new URL(`https://${trimmed}`));
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
@@ -63,11 +63,16 @@ export type TrustRulesListIpcParams = z.infer<
63
63
  typeof TrustRulesListIpcParamsSchema
64
64
  >;
65
65
 
66
+ /** The three risk levels a trust rule can set and a registry default can carry. */
67
+ export const RISK_LEVEL_VALUES = ["low", "medium", "high"] as const;
68
+ export type RiskLevelValue = (typeof RISK_LEVEL_VALUES)[number];
69
+ export const RiskLevelValueSchema = z.enum(RISK_LEVEL_VALUES);
70
+
66
71
  export const TrustRuleSchema = z.object({
67
72
  id: z.string(),
68
73
  tool: z.string(),
69
74
  pattern: z.string(),
70
- risk: z.enum(["low", "medium", "high"]),
75
+ risk: RiskLevelValueSchema,
71
76
  description: z.string(),
72
77
  origin: z.enum(["default", "user_defined"]),
73
78
  userModified: z.boolean(),
@@ -341,3 +346,163 @@ export const GetGuardianContactIpcResponseSchema = z.object({
341
346
  export type GetGuardianContactIpcResponse = z.infer<
342
347
  typeof GetGuardianContactIpcResponseSchema
343
348
  >;
349
+
350
+ // ── classify_risk ────────────────────────────────────────────────────────────
351
+ // Risk classification is gateway-owned; the assistant sends one request per
352
+ // tool invocation and reads the whole answer back. The gateway validates the
353
+ // request against `ClassifyRiskIpcParamsSchema`; the assistant validates the
354
+ // response against `ClassifyRiskIpcResponseSchema` and fails closed on a
355
+ // mismatch.
356
+
357
+ /** A classified risk; `unknown` is the classifier's own "not in registry". */
358
+ export const ClassifiedRiskSchema = z.enum([...RISK_LEVEL_VALUES, "unknown"]);
359
+ export type ClassifiedRisk = z.infer<typeof ClassifiedRiskSchema>;
360
+
361
+ /** How a risk was determined: a user trust rule, the registry, or neither. */
362
+ export const RiskMatchTypeSchema = z.enum(["user_rule", "registry", "unknown"]);
363
+ export type RiskMatchType = z.infer<typeof RiskMatchTypeSchema>;
364
+
365
+ /**
366
+ * File classifier context the assistant pre-resolves (it owns the workspace
367
+ * filesystem) so the gateway can classify file tools without assistant path
368
+ * helpers. Directories are canonicalized (symlinks resolved) by the sender.
369
+ */
370
+ export const ClassifyRiskFileContextSchema = z.object({
371
+ protectedDir: z.string(),
372
+ deprecatedDir: z.string(),
373
+ hooksDir: z.string(),
374
+ pluginsDir: z.string().optional(),
375
+ toolsDir: z.string().optional(),
376
+ routesDir: z.string().optional(),
377
+ workflowsDir: z.string().optional(),
378
+ /** Monitoring data dir: the sentinel lives here, writes are code-injection risk. */
379
+ monitoringDir: z.string().optional(),
380
+ actorTokenSigningKeyPath: z.string(),
381
+ skillSourceDirs: z.array(z.string()),
382
+ });
383
+ export type ClassifyRiskFileContext = z.infer<
384
+ typeof ClassifyRiskFileContextSchema
385
+ >;
386
+
387
+ /** Skill metadata the assistant pre-resolves for skill-load classification. */
388
+ export const ClassifyRiskSkillMetadataSchema = z.object({
389
+ skillId: z.string(),
390
+ selector: z.string(),
391
+ versionHash: z.string(),
392
+ transitiveHash: z.string().optional(),
393
+ hasInlineExpansions: z.boolean(),
394
+ isDynamic: z.boolean(),
395
+ });
396
+ export type ClassifyRiskSkillMetadata = z.infer<
397
+ typeof ClassifyRiskSkillMetadataSchema
398
+ >;
399
+
400
+ export const ClassifyRiskIpcParamsSchema = z.object({
401
+ tool: z.string().min(1),
402
+ command: z.string().optional(),
403
+ url: z.string().optional(),
404
+ path: z.string().optional(),
405
+ /**
406
+ * The file tool's target path with symlinks resolved by the assistant. The
407
+ * gateway's security escalation prefix checks use it so a symlink cannot
408
+ * mask a write into a protected directory; falls back to lexical resolution
409
+ * of `path` when absent.
410
+ */
411
+ resolvedPath: z.string().optional(),
412
+ /**
413
+ * The sandbox file tool's working directory with symlinks resolved. Paired
414
+ * with `resolvedPath` for the workspace-boundary check so a symlinked
415
+ * workspace prefix (macOS `/var` → `/private/var`) does not read as an
416
+ * escape. Absent for host tools.
417
+ */
418
+ resolvedWorkingDir: z.string().optional(),
419
+ skill: z.string().optional(),
420
+ mode: z.string().optional(),
421
+ script: z.string().optional(),
422
+ workingDir: z.string().optional(),
423
+ allowPrivateNetwork: z.boolean().optional(),
424
+ networkMode: z.string().optional(),
425
+ isContainerized: z.boolean().optional(),
426
+ workspaceRoot: z.string().optional(),
427
+ fileContext: ClassifyRiskFileContextSchema.optional(),
428
+ skillMetadata: ClassifyRiskSkillMetadataSchema.optional(),
429
+ /**
430
+ * The tool's registry default risk, for tools with no dedicated classifier.
431
+ * The gateway answers with it (`matchType: "registry"`); absent, the tool is
432
+ * unknown and answers `medium`.
433
+ */
434
+ registryDefaultRisk: RiskLevelValueSchema.optional(),
435
+ /** Number of credential references attached to this tool invocation. */
436
+ credentialRefCount: z.number().int().nonnegative().optional(),
437
+ /**
438
+ * For `host_file_transfer` to_sandbox: the workspace-side destination path
439
+ * and the sandbox working directory it resolves against, so a transfer that
440
+ * lands an executable file in a code-injection sink (tools, routes, hooks,
441
+ * plugins, skills) escalates even though `path` carries the host-side source.
442
+ */
443
+ transferSandboxDestPath: z.string().optional(),
444
+ transferSandboxWorkingDir: z.string().optional(),
445
+ /** `transferSandboxDestPath` with symlinks resolved by the assistant. */
446
+ resolvedTransferDestPath: z.string().optional(),
447
+ });
448
+ export type ClassifyRiskIpcParams = z.infer<typeof ClassifyRiskIpcParamsSchema>;
449
+
450
+ /** Regex ladder shown when the user saves a classification (display only). */
451
+ export const RiskPatternScopeOptionSchema = z.object({
452
+ pattern: z.string(),
453
+ label: z.string(),
454
+ });
455
+ export type RiskPatternScopeOption = z.infer<
456
+ typeof RiskPatternScopeOptionSchema
457
+ >;
458
+
459
+ /** Minimatch ladder for the "always allow" prompt option (what a rule matches). */
460
+ export const RiskAllowlistOptionSchema = z.object({
461
+ label: z.string(),
462
+ description: z.string(),
463
+ pattern: z.string(),
464
+ });
465
+ export type RiskAllowlistOption = z.infer<typeof RiskAllowlistOptionSchema>;
466
+
467
+ /** Directory ladder for filesystem operations; `scope` is a path glob or `everywhere`. */
468
+ export const RiskDirectoryScopeOptionSchema = z.object({
469
+ scope: z.string(),
470
+ label: z.string(),
471
+ });
472
+ export type RiskDirectoryScopeOption = z.infer<
473
+ typeof RiskDirectoryScopeOptionSchema
474
+ >;
475
+
476
+ export const RiskDangerousPatternSchema = z.object({
477
+ type: z.string(),
478
+ description: z.string(),
479
+ text: z.string(),
480
+ });
481
+ export type RiskDangerousPattern = z.infer<typeof RiskDangerousPatternSchema>;
482
+
483
+ export const ClassifyRiskIpcResponseSchema = z.object({
484
+ risk: ClassifiedRiskSchema,
485
+ reason: z.string(),
486
+ matchType: RiskMatchTypeSchema,
487
+ scopeOptions: z.array(RiskPatternScopeOptionSchema),
488
+ allowlistOptions: z.array(RiskAllowlistOptionSchema).optional(),
489
+ actionKeys: z.array(z.string()).optional(),
490
+ commandCandidates: z.array(z.string()).optional(),
491
+ dangerousPatterns: z.array(RiskDangerousPatternSchema).optional(),
492
+ opaqueConstructs: z.boolean().optional(),
493
+ isComplexSyntax: z.boolean().optional(),
494
+ sandboxAutoApprove: z.boolean().optional(),
495
+ /**
496
+ * Lexically-resolved path arguments from sandbox-auto-approve-eligible bash
497
+ * segments. The gateway has no filesystem access; the assistant resolves
498
+ * these through symlinks and revokes `sandboxAutoApprove` if any escapes
499
+ * the workspace.
500
+ */
501
+ sandboxPathArgs: z.array(z.string()).optional(),
502
+ directoryScopeOptions: z.array(RiskDirectoryScopeOptionSchema).optional(),
503
+ /** Fully resolved filesystem path arguments, for directory-scoped rule matching. */
504
+ resolvedPaths: z.array(z.string()).optional(),
505
+ });
506
+ export type ClassifyRiskIpcResponse = z.infer<
507
+ typeof ClassifyRiskIpcResponseSchema
508
+ >;
@@ -131,7 +131,6 @@ function buildManagedOutboundRequestId(
131
131
  chatId: payload.chatId,
132
132
  text: normalizedText,
133
133
  assistantId: payload.assistantId ?? null,
134
- chatAction: payload.chatAction ?? null,
135
134
  hasAttachments:
136
135
  Array.isArray(payload.attachments) && payload.attachments.length > 0,
137
136
  approvalRequestId: payload.approval?.requestId ?? null,
@@ -212,7 +211,6 @@ async function deliverManagedOutboundReply(
212
211
  chatId: payload.chatId,
213
212
  text: payload.text ?? null,
214
213
  assistantId: payload.assistantId ?? null,
215
- chatAction: payload.chatAction ?? null,
216
214
  hasAttachments,
217
215
  sourceUpdateId: callback.sourceUpdateId ?? null,
218
216
  },
@@ -325,7 +323,12 @@ export async function deliverChannelReply(
325
323
  ): Promise<ChannelDeliveryResult> {
326
324
  const managedCallback = parseManagedOutboundCallback(callbackUrl);
327
325
  if (managedCallback) {
328
- await deliverManagedOutboundReply(managedCallback, payload, bearerToken, log);
326
+ await deliverManagedOutboundReply(
327
+ managedCallback,
328
+ payload,
329
+ bearerToken,
330
+ log,
331
+ );
329
332
  return { ok: true };
330
333
  }
331
334
 
@@ -385,17 +388,7 @@ export async function deliverChannelReply(
385
388
  // Response may not be JSON for non-Slack channels; that's fine.
386
389
  }
387
390
 
388
- if (payload.chatAction) {
389
- log.debug(
390
- { chatId: payload.chatId, callbackUrl, chatAction: payload.chatAction },
391
- "Channel action delivered",
392
- );
393
- } else {
394
- log.info(
395
- { chatId: payload.chatId, callbackUrl },
396
- "Channel reply delivered",
397
- );
398
- }
391
+ log.info({ chatId: payload.chatId, callbackUrl }, "Channel reply delivered");
399
392
 
400
393
  return result;
401
394
  }
@@ -26,6 +26,7 @@ export {
26
26
  AttachmentMetadataSchema,
27
27
  ChannelDeliveryResultSchema,
28
28
  ChannelReplyPayloadSchema,
29
+ MessageAudienceSchema,
29
30
  PermissionRequestDetailsSchema,
30
31
  SlackStreamOpSchema,
31
32
  SlackStreamTaskSchema,
@@ -37,6 +38,7 @@ export type {
37
38
  AttachmentMetadata,
38
39
  ChannelDeliveryResult,
39
40
  ChannelReplyPayload,
41
+ MessageAudience,
40
42
  PermissionRequestDetails,
41
43
  SlackStreamOp,
42
44
  SlackStreamTask,
@@ -156,6 +156,26 @@ export const SlackStreamOpSchema = z
156
156
 
157
157
  export type SlackStreamOp = z.infer<typeof SlackStreamOpSchema>;
158
158
 
159
+ // ---------------------------------------------------------------------------
160
+ // Audience
161
+ // ---------------------------------------------------------------------------
162
+
163
+ /**
164
+ * Restricts a message to a single reader in a room that has more.
165
+ *
166
+ * A single object rather than a flag beside an id, because the two are only
167
+ * meaningful together: a channel cannot address one reader without knowing
168
+ * which. Slack reads this as `chat.postEphemeral`; a channel whose rooms hold
169
+ * one reader already has nothing to do.
170
+ */
171
+ export const MessageAudienceSchema = z.object({
172
+ kind: z.literal("oneReader"),
173
+ /** The reader, in the channel's own id space. */
174
+ userId: z.string(),
175
+ });
176
+
177
+ export type MessageAudience = z.infer<typeof MessageAudienceSchema>;
178
+
159
179
  // ---------------------------------------------------------------------------
160
180
  // Channel reply payload — the full outbound wire format
161
181
  // ---------------------------------------------------------------------------
@@ -163,43 +183,21 @@ export type SlackStreamOp = z.infer<typeof SlackStreamOpSchema>;
163
183
  export const ChannelReplyPayloadSchema = z.object({
164
184
  chatId: z.string(),
165
185
  text: z.string().optional(),
166
- /** Pre-formatted Block Kit blocks for Slack delivery. */
167
- blocks: z.array(z.custom<KnownBlock>()).optional(),
168
186
  assistantId: z.string().optional(),
169
187
  attachments: z.array(AttachmentMetadataSchema).optional(),
170
188
  approval: ApprovalUIMetadataSchema.optional(),
171
- chatAction: z.literal("typing").optional(),
172
189
  /**
173
- * When true, deliver via `chat.postEphemeral` so only the target `user`
174
- * sees the message.
190
+ * Who may see this message. Absent means everyone in the room, which is the
191
+ * only safe reading of an absent value: a message meant for one reader that
192
+ * loses its audience must not become a public one.
175
193
  */
176
- ephemeral: z.boolean().optional(),
177
- /** Slack user ID — required when `ephemeral` is true. */
178
- user: z.string().optional(),
179
- /** When provided, update an existing message instead of posting a new one. */
180
- messageTs: z.string().optional(),
181
- /** When true, the daemon generates Block Kit blocks from the text before delivery. */
182
- useBlocks: z.boolean().optional(),
183
- /** When provided, add or remove an emoji reaction on a message. */
184
- reaction: z
185
- .object({
186
- action: z.enum(["add", "remove"]),
187
- name: z.string(),
188
- messageTs: z.string(),
189
- })
190
- .optional(),
191
- /** When provided, set or clear the Slack Assistants API thread status. */
192
- assistantThreadStatus: z
193
- .object({
194
- channel: z.string(),
195
- threadTs: z.string(),
196
- status: z.string(),
197
- /** Serialized to Slack as `loading_messages`. */
198
- loadingMessages: z.array(z.string()).optional(),
199
- })
200
- .optional(),
201
- /** When provided, perform one Slack streaming operation (start/append/stop). */
202
- slackStream: SlackStreamOpSchema.optional(),
194
+ audience: MessageAudienceSchema.optional(),
195
+ /**
196
+ * Asks the channel to render the text richly rather than as plain text.
197
+ * Each channel decides what that means, and one that cannot ignores it.
198
+ * Slack is the only channel acting on it today, where it becomes Block Kit.
199
+ */
200
+ renderRichly: z.boolean().optional(),
203
201
  });
204
202
 
205
203
  export type ChannelReplyPayload = z.infer<typeof ChannelReplyPayloadSchema>;