@vellumai/assistant 0.9.0 → 0.10.0-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -125,6 +125,16 @@ mock.module("../ipc/gateway-client.js", () => ({
125
125
 
126
126
  // ── Import the module under test AFTER mocks are set up ──────────────────────
127
127
 
128
+ import {
129
+ mkdtempSync,
130
+ rmSync,
131
+ symlinkSync,
132
+ unlinkSync,
133
+ writeFileSync,
134
+ } from "node:fs";
135
+ import { tmpdir } from "node:os";
136
+ import { join } from "node:path";
137
+
128
138
  import {
129
139
  check,
130
140
  classifyRisk,
@@ -231,6 +241,47 @@ describe("Permission Checker (gateway IPC)", () => {
231
241
  expect(result2.reason).toBe("Cached test");
232
242
  });
233
243
 
244
+ test("file-tool cache misses when a symlink target is retargeted", async () => {
245
+ // File risk depends on filesystem state: the cache key folds in the
246
+ // symlink-resolved target, so the same raw input must NOT return a stale
247
+ // cached result after the symlink is pointed somewhere new.
248
+ const dir = mkdtempSync(join(tmpdir(), "risk-cache-symlink-"));
249
+ try {
250
+ const benign = join(dir, "benign.txt");
251
+ const other = join(dir, "other.txt");
252
+ writeFileSync(benign, "ok");
253
+ writeFileSync(other, "ok");
254
+ const link = join(dir, "link.txt");
255
+ symlinkSync(benign, link);
256
+
257
+ mockIpcClassifyRiskResult = {
258
+ risk: "low",
259
+ reason: "benign",
260
+ matchType: "registry",
261
+ scopeOptions: [],
262
+ };
263
+ const first = await classifyRisk("file_read", { path: link });
264
+ expect(first.level).toBe(RiskLevel.Low);
265
+
266
+ // Retarget the symlink to a different real file; raw input unchanged.
267
+ unlinkSync(link);
268
+ symlinkSync(other, link);
269
+
270
+ mockIpcClassifyRiskResult = {
271
+ risk: "high",
272
+ reason: "now sensitive",
273
+ matchType: "registry",
274
+ scopeOptions: [],
275
+ };
276
+ const second = await classifyRisk("file_read", { path: link });
277
+ // Cache must have missed and re-classified against the new target.
278
+ expect(second.level).toBe(RiskLevel.High);
279
+ expect(second.reason).toBe("now sensitive");
280
+ } finally {
281
+ rmSync(dir, { recursive: true, force: true });
282
+ }
283
+ });
284
+
234
285
  test("preserves commandCandidates from gateway response", async () => {
235
286
  mockIpcClassifyRiskResult = {
236
287
  risk: "low",
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
 
5
5
  import { getIsContainerized } from "../config/env-registry.js";
6
6
  import { getConfig } from "../config/loader.js";
@@ -16,6 +16,7 @@ import {
16
16
  looksLikePathOnlyInput,
17
17
  } from "../tools/network/url-safety.js";
18
18
  import { getTool, getToolOwner } from "../tools/registry.js";
19
+ import { resolveRealPath } from "../tools/shared/filesystem/path-policy.js";
19
20
  import type { Tool } from "../tools/types.js";
20
21
  import {
21
22
  getDeprecatedDir,
@@ -23,6 +24,8 @@ import {
23
24
  getWorkspaceDir,
24
25
  getWorkspaceHooksDir,
25
26
  getWorkspacePluginsDir,
27
+ getWorkspaceRoutesDir,
28
+ getWorkspaceToolsDir,
26
29
  } from "../util/platform.js";
27
30
  import {
28
31
  type ApprovalContext,
@@ -99,6 +102,7 @@ function riskCacheKey(
99
102
  input: Record<string, unknown>,
100
103
  workingDir?: string,
101
104
  manifestOverride?: ManifestOverride,
105
+ fsStateKey?: string,
102
106
  ): string {
103
107
  // Strip `reason` and `activity` before computing the cache key — they are
104
108
  // cosmetic status text that varies per invocation even for identical tool
@@ -111,10 +115,31 @@ function riskCacheKey(
111
115
  .update(workingDir ?? "")
112
116
  .update("\0")
113
117
  .update(manifestOverride ? JSON.stringify(manifestOverride) : "")
118
+ // For file tools, fold in the symlink-resolved target path(s). File risk
119
+ // depends on filesystem state (a symlink can be retargeted under a
120
+ // protected dir between calls), so the same raw input must miss the cache
121
+ // when its canonicalized target changes.
122
+ .update("\0")
123
+ .update(fsStateKey ?? "")
114
124
  .digest("hex");
115
125
  return `${toolName}\0${hash}`;
116
126
  }
117
127
 
128
+ /**
129
+ * Compute the filesystem-state component of the risk cache key for file tools:
130
+ * the symlink-resolved target path(s). Returns `undefined` for non-file tools
131
+ * (whose risk does not depend on filesystem state).
132
+ */
133
+ function fileToolFsStateKey(
134
+ toolName: string,
135
+ input: Record<string, unknown>,
136
+ workingDir?: string,
137
+ ): string | undefined {
138
+ if (!FILE_TOOL_NAMES.has(toolName)) return undefined;
139
+ const resolved = resolveFileToolPaths(toolName, input, workingDir);
140
+ return `${resolved.resolvedPath ?? ""}\0${resolved.resolvedTransferDestPath ?? ""}`;
141
+ }
142
+
118
143
  /** Clear the risk classification cache. Called when trust rules change. Exported for test setup. */
119
144
  export function clearRiskCache(): void {
120
145
  riskCache.clear();
@@ -273,16 +298,153 @@ import type {
273
298
 
274
299
  function buildFileContext(): FileContext {
275
300
  const config = getConfig();
301
+ // Canonicalize the protected directories via realpath so that a symlinked
302
+ // component anywhere in their path still prefix-matches the canonicalized
303
+ // target path computed in buildClassifyRiskParams. Both sides must be
304
+ // symlink-resolved for the gateway's lexical prefix checks to be sound.
305
+ const protectedDir = resolveRealPath(getProtectedDir());
276
306
  return {
277
- protectedDir: getProtectedDir(),
278
- deprecatedDir: getDeprecatedDir(),
279
- hooksDir: getWorkspaceHooksDir(),
280
- pluginsDir: getWorkspacePluginsDir(),
281
- actorTokenSigningKeyPath: join(
282
- getProtectedDir(),
283
- "actor-token-signing-key",
307
+ protectedDir,
308
+ deprecatedDir: resolveRealPath(getDeprecatedDir()),
309
+ hooksDir: resolveRealPath(getWorkspaceHooksDir()),
310
+ pluginsDir: resolveRealPath(getWorkspacePluginsDir()),
311
+ toolsDir: resolveRealPath(getWorkspaceToolsDir()),
312
+ routesDir: resolveRealPath(getWorkspaceRoutesDir()),
313
+ actorTokenSigningKeyPath: join(protectedDir, "actor-token-signing-key"),
314
+ skillSourceDirs: getSkillRoots(config.skills.load.extraDirs).map(
315
+ resolveRealPath,
284
316
  ),
285
- skillSourceDirs: getSkillRoots(config.skills.load.extraDirs),
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Canonicalize the security-sensitive path of a file tool invocation by
322
+ * resolving symlinks before it is sent to the gateway risk classifier.
323
+ *
324
+ * The gateway classifies file risk by lexically prefix-matching the target
325
+ * path against protected directories (skill source, hooks, plugins, the actor
326
+ * token signing key). Lexical resolution alone does not follow symlinks, so a
327
+ * symlink whose name looks benign but whose real target is a protected
328
+ * directory would be under-classified and could skip the High-risk approval
329
+ * prompt. Resolving symlinks here — on the daemon, which owns the workspace
330
+ * filesystem — closes that gap while keeping the gateway free of filesystem
331
+ * access (it cannot see the workspace in Docker mode).
332
+ *
333
+ * `resolveRealPath` falls back to the lexical path when the target lives on a
334
+ * filesystem this process cannot see (e.g. host_file paths proxied to a remote
335
+ * client), so this never regresses below today's lexical behavior.
336
+ */
337
+ // The Docker sandbox mounts the workspace at /workspace, and the model emits
338
+ // container-scoped paths (e.g. "/workspace/tools/evil.ts") even on local turns.
339
+ // Mirror the gateway's resolveSandboxPath remap so the symlink-resolved path we
340
+ // forward lines up with the gateway's lexical fallback and the protected dirs.
341
+ const CONTAINER_WORKSPACE_PREFIX = "/workspace/";
342
+ const CONTAINER_WORKSPACE_EXACT = "/workspace";
343
+
344
+ function resolveSandboxBase(rawPath: string, workingDir: string): string {
345
+ let effectivePath = rawPath;
346
+ if (!rawPath.startsWith(workingDir + "/") && rawPath !== workingDir) {
347
+ if (rawPath.startsWith(CONTAINER_WORKSPACE_PREFIX)) {
348
+ effectivePath = rawPath.slice(CONTAINER_WORKSPACE_PREFIX.length);
349
+ } else if (rawPath === CONTAINER_WORKSPACE_EXACT) {
350
+ effectivePath = ".";
351
+ }
352
+ }
353
+ return resolve(workingDir, effectivePath);
354
+ }
355
+
356
+ function resolveClassificationPath(
357
+ filePath: string,
358
+ workingDir: string,
359
+ isHostTool: boolean,
360
+ ): string | undefined {
361
+ if (!filePath) return undefined;
362
+ // Mirror the gateway classifier's lexical base: host tools resolve the path
363
+ // as absolute/relative-to-cwd; sandbox tools apply the /workspace remap and
364
+ // resolve against workingDir. Then follow symlinks so a benign-looking name
365
+ // whose real target is a protected directory is still escalated.
366
+ const base = isHostTool
367
+ ? resolve(filePath)
368
+ : resolveSandboxBase(filePath, workingDir);
369
+ return resolveRealPath(base);
370
+ }
371
+
372
+ const FILE_TOOL_NAMES = new Set([
373
+ "file_read",
374
+ "file_write",
375
+ "file_edit",
376
+ "host_file_read",
377
+ "host_file_write",
378
+ "host_file_edit",
379
+ "host_file_transfer",
380
+ ]);
381
+
382
+ interface FileToolResolution {
383
+ filePath: string;
384
+ effectiveWorkingDir: string;
385
+ isHostTool: boolean;
386
+ resolvedPath?: string;
387
+ transferSandboxDestPath?: string;
388
+ transferSandboxWorkingDir?: string;
389
+ resolvedTransferDestPath?: string;
390
+ }
391
+
392
+ /**
393
+ * Resolve the security-sensitive path(s) of a file tool invocation, including
394
+ * symlink canonicalization. Shared by the IPC param builder and the risk cache
395
+ * key so both observe the same filesystem state — file risk now depends on
396
+ * symlink targets, so the cache must key on the canonicalized path, not just
397
+ * the raw tool input.
398
+ */
399
+ function resolveFileToolPaths(
400
+ toolName: string,
401
+ input: Record<string, unknown>,
402
+ workingDir?: string,
403
+ ): FileToolResolution {
404
+ const isHostTool = toolName.startsWith("host_");
405
+ let filePath: string;
406
+ // For host_file_transfer to_sandbox, the file is written into the workspace
407
+ // at dest_path — capture it (plus the sandbox working dir) so the gateway can
408
+ // escalate writes that land in a code-injection sink, since `path` carries
409
+ // the host-side source.
410
+ let transferSandboxDestPath: string | undefined;
411
+ let transferSandboxWorkingDir: string | undefined;
412
+ if (toolName === "host_file_transfer") {
413
+ // The security-sensitive host-side path is source_path when reading from
414
+ // the host (to_sandbox), dest_path when writing to the host (to_host).
415
+ const direction = getStringField(input, "direction");
416
+ if (direction === "to_sandbox") {
417
+ filePath = getStringField(input, "source_path");
418
+ transferSandboxDestPath = getStringField(input, "dest_path");
419
+ transferSandboxWorkingDir = workingDir ?? process.cwd();
420
+ } else {
421
+ filePath = getStringField(input, "dest_path");
422
+ }
423
+ } else {
424
+ filePath = getStringField(input, "path", "file_path");
425
+ }
426
+ const effectiveWorkingDir = isHostTool ? "/" : (workingDir ?? process.cwd());
427
+ return {
428
+ filePath,
429
+ effectiveWorkingDir,
430
+ isHostTool,
431
+ resolvedPath: resolveClassificationPath(
432
+ filePath,
433
+ effectiveWorkingDir,
434
+ isHostTool,
435
+ ),
436
+ transferSandboxDestPath,
437
+ transferSandboxWorkingDir,
438
+ // The to_sandbox destination is a workspace write — symlink-resolve it too
439
+ // so it can't mask a code-injection sink.
440
+ resolvedTransferDestPath:
441
+ transferSandboxDestPath != null
442
+ ? resolveClassificationPath(
443
+ transferSandboxDestPath,
444
+ transferSandboxWorkingDir ?? process.cwd(),
445
+ false,
446
+ )
447
+ : undefined,
286
448
  };
287
449
  }
288
450
 
@@ -347,25 +509,16 @@ function buildClassifyRiskParams(
347
509
  "host_file_transfer",
348
510
  ].includes(toolName)
349
511
  ) {
350
- const isHostTool = toolName.startsWith("host_");
351
- let filePath: string;
352
- if (toolName === "host_file_transfer") {
353
- // For host_file_transfer the security-sensitive path is the host-side
354
- // path: source_path when reading from the host (to_sandbox), dest_path
355
- // when writing to the host (to_host).
356
- const direction = getStringField(input, "direction");
357
- filePath =
358
- direction === "to_sandbox"
359
- ? getStringField(input, "source_path")
360
- : getStringField(input, "dest_path");
361
- } else {
362
- filePath = getStringField(input, "path", "file_path");
363
- }
512
+ const resolved = resolveFileToolPaths(toolName, input, workingDir);
364
513
  return {
365
514
  tool: toolName,
366
- path: filePath,
367
- workingDir: isHostTool ? "/" : (workingDir ?? process.cwd()),
515
+ path: resolved.filePath,
516
+ resolvedPath: resolved.resolvedPath,
517
+ workingDir: resolved.effectiveWorkingDir,
368
518
  fileContext: buildFileContext(),
519
+ transferSandboxDestPath: resolved.transferSandboxDestPath,
520
+ transferSandboxWorkingDir: resolved.transferSandboxWorkingDir,
521
+ resolvedTransferDestPath: resolved.resolvedTransferDestPath,
369
522
  };
370
523
  }
371
524
 
@@ -448,7 +601,13 @@ export async function classifyRisk(
448
601
  signal?.throwIfAborted();
449
602
 
450
603
  // Check cache first.
451
- const cacheKey = riskCacheKey(toolName, input, workingDir, manifestOverride);
604
+ const cacheKey = riskCacheKey(
605
+ toolName,
606
+ input,
607
+ workingDir,
608
+ manifestOverride,
609
+ fileToolFsStateKey(toolName, input, workingDir),
610
+ );
452
611
  const cached = riskCache.get(cacheKey);
453
612
  if (cached !== undefined) {
454
613
  // LRU refresh
@@ -53,6 +53,8 @@ export interface FileContext {
53
53
  deprecatedDir: string;
54
54
  hooksDir: string;
55
55
  pluginsDir: string;
56
+ toolsDir: string;
57
+ routesDir: string;
56
58
  actorTokenSigningKeyPath: string;
57
59
  skillSourceDirs: string[];
58
60
  }
@@ -81,6 +83,13 @@ export interface ClassifyRiskParams {
81
83
  command?: string;
82
84
  url?: string;
83
85
  path?: string;
86
+ /**
87
+ * The file tool's target path with symlinks resolved (canonicalized by the
88
+ * daemon, which owns the workspace filesystem). The gateway uses this for its
89
+ * security escalation prefix checks so a symlink cannot mask a write into a
90
+ * protected directory. Falls back to lexical `path` resolution when absent.
91
+ */
92
+ resolvedPath?: string;
84
93
  skill?: string;
85
94
  mode?: string;
86
95
  script?: string;
@@ -95,4 +104,19 @@ export interface ClassifyRiskParams {
95
104
  registryDefaultRisk?: string;
96
105
  /** Number of credential references attached to this tool invocation. */
97
106
  credentialRefCount?: number;
107
+ /**
108
+ * For host_file_transfer with `direction: "to_sandbox"`: the workspace-side
109
+ * destination path and the sandbox working directory it resolves against, so
110
+ * the gateway can escalate transfers that install an executable file in a
111
+ * code-injection sink (tools/routes/hooks/plugins/skills).
112
+ */
113
+ transferSandboxDestPath?: string;
114
+ transferSandboxWorkingDir?: string;
115
+ /**
116
+ * The `to_sandbox` workspace destination with symlinks resolved
117
+ * (canonicalized by the daemon). Used for the code-injection-sink check so a
118
+ * symlinked destination cannot mask the real target. Falls back to lexical
119
+ * resolution of `transferSandboxDestPath` when absent.
120
+ */
121
+ resolvedTransferDestPath?: string;
98
122
  }
@@ -48,6 +48,8 @@ interface MockInteraction {
48
48
  orderedIds: string[];
49
49
  optionsById: Record<string, string[]>;
50
50
  };
51
+ toolUseId?: string;
52
+ questionDetails?: { entries: Array<{ id: string; question: string }> };
51
53
  }
52
54
  const _piStore = new Map<string, MockInteraction>();
53
55
  mock.module("../runtime/pending-interactions.js", () => ({
@@ -205,6 +207,31 @@ describe("QuestionPrompter", () => {
205
207
  expect(_piStore.has(req.requestId)).toBe(false);
206
208
  });
207
209
 
210
+ test("persists the full question entries on the interaction for rehydration", async () => {
211
+ // GIVEN a batched prompt with a tool-use id
212
+ const { prompter, sent } = makePrompter();
213
+
214
+ const promise = prompter.prompt({
215
+ ...threeQuestionParams,
216
+ toolUseId: "tool-q",
217
+ });
218
+ const req = sent[0] as QuestionRequestEvent;
219
+
220
+ // THEN the registered interaction carries the full entries (not just the
221
+ // id maps in `metadata`) so a history-load render can rehydrate the card
222
+ const interaction = _piStore.get(req.requestId);
223
+ expect(interaction?.toolUseId).toBe("tool-q");
224
+ expect(interaction?.questionDetails?.entries).toHaveLength(3);
225
+ expect(interaction?.questionDetails?.entries).toEqual(req.questions);
226
+
227
+ resolveBatch(req.requestId, [
228
+ { questionId: "q1", kind: "option", optionId: "a" },
229
+ { questionId: "q2", kind: "option", optionId: "x" },
230
+ { questionId: "q3", kind: "skip" },
231
+ ]);
232
+ await promise;
233
+ });
234
+
208
235
  test("free-text resolution", async () => {
209
236
  const { prompter, sent } = makePrompter();
210
237
 
@@ -269,6 +269,10 @@ export class QuestionPrompter {
269
269
  timer,
270
270
  toolUseId,
271
271
  metadata: { orderedIds, optionsById } satisfies QuestionBatchMetadata,
272
+ // Persist the full entries (not just the id maps in `metadata`) so a
273
+ // history-load render can stamp this outstanding prompt back onto its
274
+ // tool call and rehydrate the question card after a reconnect.
275
+ questionDetails: { entries },
272
276
  });
273
277
 
274
278
  // Populate both shapes on the wire: `questions[]` is the canonical
@@ -178,4 +178,123 @@ describe("VellumPlatformClient", () => {
178
178
  });
179
179
  });
180
180
  });
181
+
182
+ describe("getOwnerConsent()", () => {
183
+ test("maps snake_case body to camelCase on 200", async () => {
184
+ globalThis.fetch = mock(async (url: string | URL | Request) => {
185
+ expect(String(url)).toBe(
186
+ "https://platform.example.com/v1/assistants/asst-123/owner-consent/",
187
+ );
188
+ return new Response(
189
+ JSON.stringify({
190
+ share_analytics: true,
191
+ share_diagnostics: false,
192
+ share_diagnostics_accepted_version: "2026-06-18",
193
+ }),
194
+ { status: 200 },
195
+ );
196
+ }) as unknown as typeof globalThis.fetch;
197
+
198
+ const client = await VellumPlatformClient.create();
199
+ const consent = await client!.getOwnerConsent();
200
+ expect(consent).toEqual({
201
+ shareAnalytics: true,
202
+ shareDiagnostics: false,
203
+ shareDiagnosticsAcceptedVersion: "2026-06-18",
204
+ });
205
+ });
206
+
207
+ test("defaults shareDiagnosticsAcceptedVersion to '' when the platform omits it (back-compat)", async () => {
208
+ globalThis.fetch = mock(
209
+ async () =>
210
+ new Response(
211
+ JSON.stringify({ share_analytics: true, share_diagnostics: true }),
212
+ { status: 200 },
213
+ ),
214
+ ) as unknown as typeof globalThis.fetch;
215
+
216
+ const client = await VellumPlatformClient.create();
217
+ const consent = await client!.getOwnerConsent();
218
+ expect(consent).toEqual({
219
+ shareAnalytics: true,
220
+ shareDiagnostics: true,
221
+ shareDiagnosticsAcceptedVersion: "",
222
+ });
223
+ });
224
+
225
+ test("uses the authenticated Api-Key header", async () => {
226
+ globalThis.fetch = mock(
227
+ async (_url: string | URL | Request, init?: RequestInit) => {
228
+ const headers = new Headers(init?.headers);
229
+ expect(headers.get("Authorization")).toBe("Api-Key sk-test-key");
230
+ return new Response(
231
+ JSON.stringify({ share_analytics: true, share_diagnostics: true }),
232
+ { status: 200 },
233
+ );
234
+ },
235
+ ) as unknown as typeof globalThis.fetch;
236
+
237
+ const client = await VellumPlatformClient.create();
238
+ await client!.getOwnerConsent();
239
+ });
240
+
241
+ test("returns null on 404", async () => {
242
+ globalThis.fetch = mock(
243
+ async () => new Response("not found", { status: 404 }),
244
+ ) as unknown as typeof globalThis.fetch;
245
+
246
+ const client = await VellumPlatformClient.create();
247
+ expect(await client!.getOwnerConsent()).toBeNull();
248
+ });
249
+
250
+ test("returns null on 500", async () => {
251
+ globalThis.fetch = mock(
252
+ async () => new Response("error", { status: 500 }),
253
+ ) as unknown as typeof globalThis.fetch;
254
+
255
+ const client = await VellumPlatformClient.create();
256
+ expect(await client!.getOwnerConsent()).toBeNull();
257
+ });
258
+
259
+ test("returns null on network error", async () => {
260
+ globalThis.fetch = mock(async () => {
261
+ throw new Error("network down");
262
+ }) as unknown as typeof globalThis.fetch;
263
+
264
+ const client = await VellumPlatformClient.create();
265
+ expect(await client!.getOwnerConsent()).toBeNull();
266
+ });
267
+
268
+ test("returns null on malformed body (non-boolean fields)", async () => {
269
+ globalThis.fetch = mock(
270
+ async () =>
271
+ new Response(
272
+ JSON.stringify({ share_analytics: "yes", share_diagnostics: 1 }),
273
+ { status: 200 },
274
+ ),
275
+ ) as unknown as typeof globalThis.fetch;
276
+
277
+ const client = await VellumPlatformClient.create();
278
+ expect(await client!.getOwnerConsent()).toBeNull();
279
+ });
280
+
281
+ test("returns null without fetching when assistantId is empty", async () => {
282
+ mockAssistantId = "";
283
+ mockSecureKeys = {};
284
+
285
+ const fetchSpy = mock(
286
+ async () =>
287
+ new Response(
288
+ JSON.stringify({ share_analytics: true, share_diagnostics: true }),
289
+ { status: 200 },
290
+ ),
291
+ );
292
+ globalThis.fetch = fetchSpy as unknown as typeof globalThis.fetch;
293
+
294
+ const client = await VellumPlatformClient.create();
295
+ expect(client!.platformAssistantId).toBe("");
296
+ expect(await client!.getOwnerConsent()).toBeNull();
297
+ expect(fetchSpy).not.toHaveBeenCalled();
298
+ });
299
+ });
181
300
  });
@@ -16,6 +16,18 @@ const log = getLogger("platform-client");
16
16
 
17
17
  let _missingPrereqsWarned = false;
18
18
 
19
+ export interface OwnerConsent {
20
+ shareAnalytics: boolean;
21
+ shareDiagnostics: boolean;
22
+ /**
23
+ * Version of the diagnostics-sharing consent the owner accepted
24
+ * ("YYYY-MM-DD", or "" if never accepted). Composes the per-turn
25
+ * trace-collection gate: traces are only collected once this is >= the
26
+ * disclosing version (see telemetry/trace-collection-policy.ts).
27
+ */
28
+ shareDiagnosticsAcceptedVersion: string;
29
+ }
30
+
19
31
  export class VellumPlatformClient {
20
32
  private readonly platformBaseUrl: string;
21
33
  private readonly apiKey: string;
@@ -109,6 +121,60 @@ export class VellumPlatformClient {
109
121
  return fetch(url, { ...init, headers });
110
122
  }
111
123
 
124
+ /**
125
+ * Fetch the platform owner's telemetry consent for this assistant.
126
+ *
127
+ * Returns `null` whenever the consent is unknown — missing assistant id,
128
+ * any non-2xx response (e.g. 404 before the endpoint is deployed), a
129
+ * malformed body, or a network error. Callers treat `null` as default-off.
130
+ * Never throws.
131
+ */
132
+ async getOwnerConsent(): Promise<OwnerConsent | null> {
133
+ if (!this.assistantId) {
134
+ return null;
135
+ }
136
+
137
+ try {
138
+ const res = await this.fetch(
139
+ `/v1/assistants/${this.assistantId}/owner-consent/`,
140
+ );
141
+ if (!res.ok) {
142
+ log.debug(
143
+ { status: res.status },
144
+ "owner-consent fetch returned non-2xx — treating as unknown",
145
+ );
146
+ return null;
147
+ }
148
+
149
+ const body = (await res.json()) as {
150
+ share_analytics?: unknown;
151
+ share_diagnostics?: unknown;
152
+ share_diagnostics_accepted_version?: unknown;
153
+ };
154
+ if (
155
+ typeof body.share_analytics !== "boolean" ||
156
+ typeof body.share_diagnostics !== "boolean"
157
+ ) {
158
+ log.debug("owner-consent body malformed — treating as unknown");
159
+ return null;
160
+ }
161
+
162
+ return {
163
+ shareAnalytics: body.share_analytics,
164
+ shareDiagnostics: body.share_diagnostics,
165
+ // Back-compat: an older platform that doesn't return this field yields
166
+ // "" → fails the trace-collection version gate → fail-closed (no trace).
167
+ shareDiagnosticsAcceptedVersion:
168
+ typeof body.share_diagnostics_accepted_version === "string"
169
+ ? body.share_diagnostics_accepted_version
170
+ : "",
171
+ };
172
+ } catch (err) {
173
+ log.debug({ err }, "owner-consent fetch failed — treating as unknown");
174
+ return null;
175
+ }
176
+ }
177
+
112
178
  get baseUrl(): string {
113
179
  return this.platformBaseUrl;
114
180
  }