@vellumai/assistant 0.11.6 → 0.11.7-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/environments/package.json +2 -1
  21. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  22. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  30. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  32. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  33. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  34. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  35. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  36. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  38. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  39. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  40. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  41. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  42. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  43. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  44. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  45. package/openapi.yaml +419 -55
  46. package/package.json +4 -1
  47. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  48. package/src/__tests__/approval-card-edit.test.ts +153 -0
  49. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  50. package/src/__tests__/available-skills.test.ts +54 -0
  51. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  52. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  53. package/src/__tests__/channel-policy.test.ts +1 -3
  54. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  55. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  56. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  57. package/src/__tests__/config-schema.test.ts +2 -3
  58. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  59. package/src/__tests__/conversation-attachments.test.ts +0 -1
  60. package/src/__tests__/conversation-error.test.ts +44 -0
  61. package/src/__tests__/conversation-evictor.test.ts +19 -0
  62. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  63. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  65. package/src/__tests__/conversation-slash.test.ts +9 -0
  66. package/src/__tests__/conversation-title-service.test.ts +97 -0
  67. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  68. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  69. package/src/__tests__/credential-broker.test.ts +43 -420
  70. package/src/__tests__/credential-routes.test.ts +17 -1
  71. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  72. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  73. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  74. package/src/__tests__/discord-channel-config.test.ts +237 -0
  75. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  76. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  77. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  78. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  79. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  80. package/src/__tests__/image-conversion.test.ts +38 -4
  81. package/src/__tests__/install-skill-routing.test.ts +25 -0
  82. package/src/__tests__/llm-resolver.test.ts +74 -0
  83. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  84. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  85. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  86. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  87. package/src/__tests__/mcp-cli.test.ts +23 -1
  88. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  89. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  90. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  91. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  92. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  93. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  94. package/src/__tests__/platform.test.ts +19 -1
  95. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  96. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  97. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  98. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  99. package/src/__tests__/schedule-retry.test.ts +183 -4
  100. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  101. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  102. package/src/__tests__/skill-load-tool.test.ts +34 -0
  103. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  104. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  105. package/src/__tests__/skills.test.ts +25 -0
  106. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  107. package/src/__tests__/terminal-tools.test.ts +2 -0
  108. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  109. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  110. package/src/__tests__/tool-audit.test.ts +111 -40
  111. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  112. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  113. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  114. package/src/__tests__/voice-config-update.test.ts +43 -0
  115. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  116. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  117. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  118. package/src/agent/loop.ts +31 -2
  119. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  120. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  121. package/src/avatar/avatar-manifest.ts +48 -100
  122. package/src/avatar/avatar-store.ts +7 -7
  123. package/src/avatar/ensure-raster.ts +146 -0
  124. package/src/avatar/resvg-lazy.test.ts +0 -1
  125. package/src/avatar/traits-png-sync.ts +9 -10
  126. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  127. package/src/backup/__tests__/paths.test.ts +6 -2
  128. package/src/backup/paths.ts +6 -4
  129. package/src/bundler/app-compiler.ts +2 -2
  130. package/src/bundler/compiler-tools.test.ts +36 -0
  131. package/src/bundler/compiler-tools.ts +54 -18
  132. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
  134. package/src/calls/media-stream-stt-session.ts +2 -1
  135. package/src/calls/telephony-synthesis-language.ts +13 -5
  136. package/src/calls/voice-session-bridge.ts +6 -4
  137. package/src/channels/config.ts +23 -12
  138. package/src/cli/AGENTS.md +3 -1
  139. package/src/cli/commands/backup.help.ts +10 -13
  140. package/src/cli/commands/contacts.help.ts +16 -4
  141. package/src/cli/commands/contacts.ts +5 -1
  142. package/src/cli/commands/credentials.help.ts +1 -1
  143. package/src/cli/commands/inference-providers.ts +18 -53
  144. package/src/cli/commands/mcp.help.ts +6 -5
  145. package/src/cli/commands/mcp.ts +1 -1
  146. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  147. package/src/cli/commands/stt.help.ts +2 -2
  148. package/src/config/__tests__/backup-schema.test.ts +1 -1
  149. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  150. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  151. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  152. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  153. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  154. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  155. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  156. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  157. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  158. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  159. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  160. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  161. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  162. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  163. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  164. package/src/config/default-profile-catalog.ts +195 -10
  165. package/src/config/default-profile-names.ts +41 -0
  166. package/src/config/feature-flag-registry.json +18 -2
  167. package/src/config/inference-profile-validation.ts +11 -2
  168. package/src/config/llm-resolver.ts +25 -0
  169. package/src/config/loader.ts +3 -0
  170. package/src/config/managed-speech-defaults.ts +160 -10
  171. package/src/config/schema.ts +2 -0
  172. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  173. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  174. package/src/config/schemas/backup.ts +1 -1
  175. package/src/config/schemas/channels.ts +32 -0
  176. package/src/config/schemas/live-voice.ts +13 -4
  177. package/src/config/schemas/llm.ts +259 -11
  178. package/src/config/schemas/mcp.ts +10 -2
  179. package/src/config/schemas/memory-retrospective.ts +1 -19
  180. package/src/config/schemas/services.ts +24 -0
  181. package/src/config/schemas/stt.ts +128 -5
  182. package/src/config/seed-inference-profiles.ts +32 -3
  183. package/src/config/skill-state.ts +5 -0
  184. package/src/config/skills.ts +21 -0
  185. package/src/contacts/notify-contacts-changed.ts +5 -2
  186. package/src/context/compactor.ts +74 -1
  187. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  188. package/src/daemon/config-watcher.ts +2 -1
  189. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  190. package/src/daemon/conversation-agent-loop.ts +86 -10
  191. package/src/daemon/conversation-error.ts +25 -0
  192. package/src/daemon/conversation-evictor.ts +2 -4
  193. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  194. package/src/daemon/conversation-skill-tools.ts +5 -1
  195. package/src/daemon/conversation-slash.ts +2 -2
  196. package/src/daemon/conversation-store.ts +8 -1
  197. package/src/daemon/conversation-tool-setup.ts +37 -0
  198. package/src/daemon/conversation.ts +0 -4
  199. package/src/daemon/handlers/channel-config-result.ts +30 -0
  200. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  201. package/src/daemon/handlers/config-ingress.ts +9 -0
  202. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  203. package/src/daemon/handlers/config-telegram.ts +2 -5
  204. package/src/daemon/handlers/conversations.ts +0 -2
  205. package/src/daemon/handlers/skills.ts +25 -7
  206. package/src/daemon/lifecycle.ts +3 -1
  207. package/src/daemon/message-types/messages.ts +0 -2
  208. package/src/daemon/tool-setup-types.ts +5 -3
  209. package/src/daemon/trust-context-types.test.ts +33 -0
  210. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  211. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  212. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  213. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  214. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  215. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  216. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  217. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  218. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  219. package/src/live-voice/live-voice-session-manager.ts +39 -2
  220. package/src/live-voice/live-voice-session.ts +213 -8
  221. package/src/live-voice/protocol.ts +187 -1
  222. package/src/messaging/providers/discord/api.ts +2 -1
  223. package/src/messaging/providers/discord/withdraw.ts +62 -0
  224. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  225. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  226. package/src/monitoring/db-integrity-sample.ts +1 -0
  227. package/src/monitoring/file-descriptors.ts +39 -6
  228. package/src/monitoring/process-memory.ts +37 -5
  229. package/src/monitoring/resource-sampler.ts +25 -8
  230. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  231. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  232. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  233. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  234. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  235. package/src/notifications/activity-failed-dedupe.ts +95 -0
  236. package/src/notifications/adapters/discord.ts +132 -0
  237. package/src/notifications/adapters/shared.ts +16 -0
  238. package/src/notifications/adapters/slack.ts +6 -1
  239. package/src/notifications/adapters/telegram.ts +5 -8
  240. package/src/notifications/approval-card-data.ts +94 -1
  241. package/src/notifications/background-failure-signal.ts +116 -0
  242. package/src/notifications/broadcaster.ts +3 -9
  243. package/src/notifications/copy-composer.ts +41 -8
  244. package/src/notifications/decision-engine.ts +55 -0
  245. package/src/notifications/destination-resolver.ts +45 -5
  246. package/src/notifications/emit-signal.ts +19 -5
  247. package/src/notifications/guardian-question-mode.ts +57 -0
  248. package/src/notifications/types.ts +2 -8
  249. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  250. package/src/oauth/seed-providers.ts +261 -0
  251. package/src/permissions/checker.test.ts +21 -0
  252. package/src/permissions/checker.ts +6 -0
  253. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  254. package/src/permissions/gateway-threshold-reader.ts +144 -10
  255. package/src/permissions/prompter.ts +0 -6
  256. package/src/permissions/types.ts +5 -4
  257. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  258. package/src/persistence/bulk-write-gate.ts +9 -10
  259. package/src/persistence/conversation-crud.ts +85 -133
  260. package/src/persistence/conversation-title-service.ts +43 -29
  261. package/src/persistence/embeddings/embedding-local.ts +14 -85
  262. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  263. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  264. package/src/persistence/lifecycle-events-store.ts +75 -5
  265. package/src/platform/platform-patch-queue.test.ts +368 -0
  266. package/src/platform/platform-patch-queue.ts +190 -0
  267. package/src/platform/sync-avatar.test.ts +527 -0
  268. package/src/platform/sync-avatar.ts +209 -0
  269. package/src/platform/sync-identity.test.ts +75 -0
  270. package/src/platform/sync-identity.ts +17 -110
  271. package/src/plugin-api/model-profiles.test.ts +2 -2
  272. package/src/plugin-api/model-profiles.ts +7 -7
  273. package/src/plugin-api/transcription-session.ts +1 -1
  274. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  275. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  276. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  280. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  281. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  282. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  283. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  284. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  286. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  287. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  288. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  289. package/src/plugins/defaults/memory/worker.ts +7 -0
  290. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  291. package/src/providers/connection-resolution.ts +2 -17
  292. package/src/providers/fallback-breaker.ts +389 -0
  293. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  294. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  295. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  296. package/src/providers/inference/adapter-factory.ts +231 -3
  297. package/src/providers/inference/auth.ts +1 -0
  298. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  299. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  300. package/src/providers/inference/resolve-auth.ts +1 -1
  301. package/src/providers/media-resolve.ts +2 -3
  302. package/src/providers/model-catalog.ts +75 -0
  303. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  304. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  305. package/src/providers/openai/api-error-normalization.ts +13 -0
  306. package/src/providers/openai/chat-completions-provider.ts +84 -0
  307. package/src/providers/opencode/client.test.ts +150 -0
  308. package/src/providers/opencode/client.ts +61 -0
  309. package/src/providers/provider-resolvability.ts +20 -0
  310. package/src/providers/retry.ts +956 -68
  311. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  312. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  313. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  314. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  315. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  316. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  317. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  318. package/src/providers/speech-to-text/resolve.ts +132 -24
  319. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  320. package/src/providers/types.ts +39 -0
  321. package/src/providers/usage-tracking.ts +12 -1
  322. package/src/runtime/AGENTS.md +1 -2
  323. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  324. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  325. package/src/runtime/agent-wake.ts +44 -5
  326. package/src/runtime/background-job-runner.ts +68 -50
  327. package/src/runtime/channel-readiness-service.ts +0 -39
  328. package/src/runtime/http-server.ts +12 -4
  329. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  330. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  331. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  332. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  333. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  334. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  335. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  336. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  337. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  338. package/src/runtime/routes/approval-routes.ts +0 -12
  339. package/src/runtime/routes/avatar-routes.ts +17 -30
  340. package/src/runtime/routes/backup-routes.ts +2 -2
  341. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  342. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  343. package/src/runtime/routes/contact-routes.ts +20 -15
  344. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  345. package/src/runtime/routes/conversation-routes.ts +2 -2
  346. package/src/runtime/routes/credential-routes.ts +2 -1
  347. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  348. package/src/runtime/routes/inbound-message-handler.ts +21 -9
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  350. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  351. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  352. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  353. package/src/runtime/routes/index.ts +2 -0
  354. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  355. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  356. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  357. package/src/runtime/routes/inference-send-routes.ts +2 -2
  358. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  359. package/src/runtime/routes/integrations/discord.ts +119 -0
  360. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  361. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  362. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  363. package/src/runtime/routes/secret-routes.ts +7 -0
  364. package/src/runtime/routes/stt-routes.ts +38 -6
  365. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  366. package/src/runtime/routes/watch-routes.ts +9 -5
  367. package/src/runtime/sync/resource-sync-events.ts +2 -0
  368. package/src/runtime/trust-verdict-consumer.ts +3 -3
  369. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  370. package/src/schedule/retry-policy.ts +8 -8
  371. package/src/schedule/run-script.ts +14 -23
  372. package/src/schedule/schedule-recovery.ts +0 -1
  373. package/src/schedule/scheduler.ts +89 -51
  374. package/src/schedule/worker.ts +57 -0
  375. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  376. package/src/security/oauth2.ts +7 -0
  377. package/src/security/openai-device-auth.ts +364 -0
  378. package/src/skills/available-skills.ts +10 -1
  379. package/src/skills/catalog-install.ts +19 -1
  380. package/src/skills/inline-command-runner.ts +6 -2
  381. package/src/skills/platform-compatibility.test.ts +49 -0
  382. package/src/skills/platform-compatibility.ts +65 -0
  383. package/src/skills/tool-manifest.ts +20 -0
  384. package/src/stt/__tests__/roles.test.ts +152 -0
  385. package/src/stt/daemon-batch-transcriber.ts +4 -1
  386. package/src/stt/roles.ts +180 -0
  387. package/src/stt/stt-stream-session.ts +3 -2
  388. package/src/stt/types.ts +33 -3
  389. package/src/subagent/manager.ts +12 -0
  390. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  391. package/src/telemetry/live-voice-funnel.ts +12 -2
  392. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  393. package/src/telemetry/tool-audit.ts +83 -30
  394. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  395. package/src/telemetry/turn-outcome.ts +75 -4
  396. package/src/tools/client-os.ts +115 -0
  397. package/src/tools/computer-use/definitions.ts +11 -7
  398. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  399. package/src/tools/credentials/broker-types.ts +0 -71
  400. package/src/tools/credentials/broker.ts +6 -201
  401. package/src/tools/executor.ts +2 -8
  402. package/src/tools/host-shell.test.ts +75 -0
  403. package/src/tools/host-shell.ts +24 -0
  404. package/src/tools/host-terminal/host-shell.ts +15 -44
  405. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  406. package/src/tools/permission-checker.ts +73 -21
  407. package/src/tools/policy-context.ts +1 -0
  408. package/src/tools/shared/filesystem/image-read.ts +1 -1
  409. package/src/tools/skills/find-similar-skills.ts +10 -2
  410. package/src/tools/skills/load.ts +41 -6
  411. package/src/tools/skills/sandbox-runner.ts +8 -16
  412. package/src/tools/skills/skill-tool-factory.ts +1 -0
  413. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  414. package/src/tools/terminal/safe-env.ts +39 -9
  415. package/src/tools/terminal/shell.ts +12 -31
  416. package/src/tools/tool-approval-handler.ts +64 -10
  417. package/src/tools/tool-types.ts +10 -0
  418. package/src/tools/types.ts +19 -3
  419. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  420. package/src/tools/workflows/manage-workflows.ts +2 -2
  421. package/src/tools/workflows/run-workflow.test.ts +2 -1
  422. package/src/util/__tests__/file-use.test.ts +61 -0
  423. package/src/util/__tests__/process-table.test.ts +66 -0
  424. package/src/util/__tests__/process-tree.test.ts +46 -0
  425. package/src/util/errors.ts +1 -0
  426. package/src/util/file-use.ts +91 -0
  427. package/src/util/host-process.test.ts +17 -0
  428. package/src/util/host-process.ts +75 -0
  429. package/src/util/image-conversion.ts +39 -7
  430. package/src/util/platform.ts +32 -13
  431. package/src/util/process-table.ts +249 -0
  432. package/src/util/process-tree.ts +85 -129
  433. package/src/util/provider-error-patterns.ts +20 -0
  434. package/src/util/spawn.ts +9 -2
  435. package/src/workspace/git-service.ts +4 -14
  436. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  437. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  438. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  439. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  440. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  441. package/src/workspace/migrations/registry.ts +8 -0
  442. package/src/__tests__/permission-types.test.ts +0 -17
  443. package/src/channels/gateway-discord-admission.ts +0 -32
  444. package/src/persistence/fork-message-copy.ts +0 -239
  445. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  446. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  447. package/src/security/oauth2-device-code.ts +0 -312
@@ -25,11 +25,15 @@ import type {
25
25
  import { RiskLevel } from "../permissions/types.js";
26
26
  import { resolveCapabilities } from "../runtime/capabilities.js";
27
27
  import {
28
+ type PermissionPromptOutcome,
29
+ type PermissionPromptTelemetry,
28
30
  recordToolDenied,
31
+ recordToolPermissionDecided,
29
32
  recordToolPermissionPrompted,
30
33
  } from "../telemetry/tool-audit.js";
31
34
  import { getLogger } from "../util/logger.js";
32
35
  import { resolveExecutionTarget } from "./execution-target.js";
36
+ import { getHostShell } from "./host-shell.js";
33
37
  import { buildPolicyContext } from "./policy-context.js";
34
38
  import { isSideEffectTool } from "./side-effects.js";
35
39
  import type { Tool, ToolContext } from "./types.js";
@@ -124,6 +128,7 @@ export class PermissionChecker {
124
128
  undefined,
125
129
  undefined,
126
130
  context.signal,
131
+ getHostShell(context, input),
127
132
  );
128
133
  const { level: risk, reason: riskReason } = classification;
129
134
  const riskLevel: string = risk;
@@ -168,6 +173,7 @@ export class PermissionChecker {
168
173
  policyContext.conversationId,
169
174
  policyContext.executionContext,
170
175
  cellQuery,
176
+ policyContext.requesterContactId,
171
177
  );
172
178
  const riskThreshold = conversationThreshold as RiskThreshold;
173
179
 
@@ -210,7 +216,6 @@ export class PermissionChecker {
210
216
  reason: result.reason,
211
217
  riskLevel,
212
218
  durationMs: Date.now() - startTime,
213
- wasPrompted: false,
214
219
  });
215
220
  const provenance = mapApprovalProvenance("denied", {});
216
221
  return {
@@ -249,7 +254,6 @@ export class PermissionChecker {
249
254
  "Inline-command skill load requires human approval; no interactive client connected",
250
255
  riskLevel,
251
256
  durationMs: Date.now() - startTime,
252
- wasPrompted: false,
253
257
  });
254
258
  return {
255
259
  allowed: false,
@@ -316,6 +320,7 @@ export class PermissionChecker {
316
320
  context.conversationId,
317
321
  "background",
318
322
  cellQuery,
323
+ context.requesterContactId,
319
324
  );
320
325
  const thresholdOrdinal: Record<string, number> = {
321
326
  none: -1,
@@ -361,7 +366,6 @@ export class PermissionChecker {
361
366
  reason: "Non-interactive session: no client to approve prompt",
362
367
  riskLevel,
363
368
  durationMs: Date.now() - startTime,
364
- wasPrompted: false,
365
369
  });
366
370
  return {
367
371
  allowed: false,
@@ -385,27 +389,76 @@ export class PermissionChecker {
385
389
  persistentDecisionsAllowed: !context.requireFreshApproval,
386
390
  };
387
391
 
388
- recordToolPermissionPrompted(name);
389
-
390
- const response = await this.prompter.prompt(
391
- name,
392
- input,
392
+ // Grouping dimensions shared by this prompt and its decision, so the
393
+ // two series can be diffed by risk and by access preset without
394
+ // parsing anything out of the event name.
395
+ const promptTelemetry: PermissionPromptTelemetry = {
396
+ toolName: name,
393
397
  riskLevel,
394
- promptOptions.allowlistOptions,
395
- promptOptions.scopeOptions,
396
- previewDiff,
397
- context.conversationId,
398
- executionTarget,
399
- promptOptions.persistentDecisionsAllowed,
400
- context.signal,
401
- context.toolUseId,
402
- riskReason,
403
- getIsContainerized(),
404
- classification.directoryScopeOptions,
405
- );
398
+ riskThreshold,
399
+ surface: context.executionChannel,
400
+ conversationId: context.conversationId,
401
+ };
402
+ // An already-aborted signal makes the prompter return without
403
+ // registering or sending a confirmation request, so nothing reaches
404
+ // the user and neither half of the pair is recorded. `prompt()` reads
405
+ // the same signal synchronously on entry, with no await in between, so
406
+ // this check and its check always agree.
407
+ const promptIsSurfaced = context.signal?.aborted !== true;
408
+ if (promptIsSurfaced) {
409
+ recordToolPermissionPrompted(promptTelemetry);
410
+ }
411
+
412
+ let response: Awaited<ReturnType<PermissionPrompter["prompt"]>>;
413
+ try {
414
+ response = await this.prompter.prompt(
415
+ name,
416
+ input,
417
+ riskLevel,
418
+ promptOptions.allowlistOptions,
419
+ promptOptions.scopeOptions,
420
+ previewDiff,
421
+ context.conversationId,
422
+ executionTarget,
423
+ promptOptions.persistentDecisionsAllowed,
424
+ context.signal,
425
+ context.toolUseId,
426
+ riskReason,
427
+ getIsContainerized(),
428
+ classification.directoryScopeOptions,
429
+ );
430
+ } catch (err) {
431
+ // The prompter rejected rather than resolved: it was disposed while
432
+ // this prompt was outstanding (client disconnect, conversation
433
+ // teardown). Nobody answered, so the prompt is abandoned.
434
+ if (promptIsSurfaced) {
435
+ recordToolPermissionDecided(promptTelemetry, "abandoned");
436
+ }
437
+ throw err;
438
+ }
406
439
 
407
440
  const decision = response.decision;
408
441
 
442
+ // A prompt that ends without a human answer is abandoned, not denied.
443
+ // The daemon resolves a timeout, a superseding user message, and a
444
+ // turn abort to `deny` so the agent loop stops, but none of the three
445
+ // is a decision the user made.
446
+ let outcome: PermissionPromptOutcome;
447
+ if (
448
+ response.wasTimeout === true ||
449
+ response.wasSystemCancel === true ||
450
+ response.wasAbort === true
451
+ ) {
452
+ outcome = "abandoned";
453
+ } else if (decision === "deny") {
454
+ outcome = "deny";
455
+ } else {
456
+ outcome = "allow";
457
+ }
458
+ if (promptIsSurfaced) {
459
+ recordToolPermissionDecided(promptTelemetry, outcome);
460
+ }
461
+
409
462
  if (decision === "deny") {
410
463
  const contextualDenial =
411
464
  typeof response.decisionContext === "string"
@@ -426,7 +479,6 @@ export class PermissionChecker {
426
479
  reason: denialReason,
427
480
  riskLevel,
428
481
  durationMs: Date.now() - startTime,
429
- wasPrompted: true,
430
482
  });
431
483
  return {
432
484
  allowed: false,
@@ -62,6 +62,7 @@ export function buildPolicyContext(
62
62
  const originSignals = {
63
63
  requestOrigin: context?.requestOrigin,
64
64
  ...channelCoordinatesFromToolContext(context),
65
+ requesterContactId: context?.requesterContactId,
65
66
  // Precompute the proc-to-skills gate — the v3 tier being active, i.e.
66
67
  // memory on AND v3 live — here so the permission checker, a leaf module
67
68
  // that must not read config, can deny the memory-retrospective
@@ -76,7 +76,7 @@ async function buildImageToolResult(
76
76
  }
77
77
 
78
78
  // Detect actual format from magic bytes - never trust the file extension
79
- // alone, since sips converts to JPEG and files can be misnamed.
79
+ // alone, since conversion produces JPEG and files can be misnamed.
80
80
  const detectedType = detectMediaType(buffer);
81
81
  if (!detectedType) {
82
82
  return {
@@ -3,6 +3,10 @@ import { loadSkillCatalog } from "../../config/skills.js";
3
3
  import { nearestExistingSkills } from "../../plugins/defaults/memory/v3/candidate-match.js";
4
4
  import { readInstallMeta } from "../../skills/install-meta.js";
5
5
  import { getManagedSkillDir } from "../../skills/managed-store.js";
6
+ import {
7
+ filterSkillsByPlatform,
8
+ type SkillPlatform,
9
+ } from "../../skills/platform-compatibility.js";
6
10
  import type { OwnerInfo, ToolContext, ToolExecutionResult } from "../types.js";
7
11
 
8
12
  /**
@@ -45,6 +49,7 @@ export async function executeFindSimilarSkills(
45
49
  description: string;
46
50
  source: SkillSource;
47
51
  owner?: OwnerInfo;
52
+ platforms?: SkillPlatform[];
48
53
  }[];
49
54
  } = {},
50
55
  ): Promise<ToolExecutionResult> {
@@ -92,8 +97,11 @@ export async function executeFindSimilarSkills(
92
97
  // slices only those. Filtering after the limit would let out-of-scope
93
98
  // high-rank matches consume slots and starve usable in-scope skills out of
94
99
  // the result. `null` set = pass the catalog through unchanged.
95
- const scopedCatalog =
96
- enabledPluginSet === null ? catalog : catalog.filter((s) => !outOfScope(s));
100
+ const scopedCatalog = filterSkillsByPlatform(
101
+ enabledPluginSet === null
102
+ ? catalog
103
+ : catalog.filter((skill) => !outOfScope(skill)),
104
+ );
97
105
 
98
106
  const hits = await findNearest(goal, {
99
107
  limit,
@@ -21,10 +21,15 @@ import {
21
21
  validateIncludeCycles,
22
22
  } from "../../skills/include-graph.js";
23
23
  import { renderInlineCommands } from "../../skills/inline-command-render.js";
24
+ import {
25
+ isSkillCompatibleWithPlatform,
26
+ skillPlatformUnavailableMessage,
27
+ } from "../../skills/platform-compatibility.js";
24
28
  import { parseToolManifestFile } from "../../skills/tool-manifest.js";
25
29
  import { computeSkillVersionHash } from "../../skills/version-hash.js";
26
30
  import { getLogger } from "../../util/logger.js";
27
31
  import { getWorkspaceDirDisplay } from "../../util/platform.js";
32
+ import { supportsClientOsForSkillTool } from "../client-os.js";
28
33
  import type {
29
34
  ToolContext,
30
35
  ToolDefinition,
@@ -83,8 +88,23 @@ function loadToolManifest(
83
88
  */
84
89
  function formatToolSchemas(
85
90
  manifest: SkillToolManifest,
91
+ context: Pick<
92
+ ToolContext,
93
+ "clientOs" | "transportInterface" | "sourceActorPrincipalId"
94
+ >,
86
95
  childSkillName?: string,
87
- ): string {
96
+ ): string | undefined {
97
+ const tools = manifest.tools.filter((tool) =>
98
+ supportsClientOsForSkillTool(tool.supported_client_os, tool.name, {
99
+ clientOs: context.clientOs,
100
+ transportInterface: context.transportInterface,
101
+ sourceActorPrincipalId: context.sourceActorPrincipalId,
102
+ }),
103
+ );
104
+ if (tools.length === 0) {
105
+ return undefined;
106
+ }
107
+
88
108
  const lines: string[] = childSkillName
89
109
  ? [`### Tools from ${childSkillName}`, ""]
90
110
  : [
@@ -96,7 +116,7 @@ function formatToolSchemas(
96
116
 
97
117
  const toolHeadingLevel = childSkillName ? "####" : "###";
98
118
 
99
- for (const tool of manifest.tools) {
119
+ for (const tool of tools) {
100
120
  lines.push(`${toolHeadingLevel} ${tool.name}`);
101
121
  lines.push(
102
122
  tool.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay()),
@@ -211,6 +231,13 @@ export const skillLoadTool = {
211
231
 
212
232
  const skill = loaded.skill;
213
233
 
234
+ if (!isSkillCompatibleWithPlatform(skill)) {
235
+ return {
236
+ content: `Error: ${skillPlatformUnavailableMessage(skill.id, skill)}`,
237
+ isError: true,
238
+ };
239
+ }
240
+
214
241
  // Per-chat plugin scope gate: a plugin-owned skill whose owning plugin is
215
242
  // outside the conversation's effective set must not have its instructions
216
243
  // loaded. Mirrors the projection/tools filter's owner-id lookup
@@ -403,7 +430,7 @@ export const skillLoadTool = {
403
430
  // Load tool schemas for the main skill
404
431
  const mainManifest = loadToolManifest(skill.directoryPath);
405
432
  const toolSchemasSection = mainManifest
406
- ? formatToolSchemas(mainManifest)
433
+ ? formatToolSchemas(mainManifest, context)
407
434
  : undefined;
408
435
 
409
436
  // Build immediate children metadata section and load included skill bodies
@@ -422,6 +449,9 @@ export const skillLoadTool = {
422
449
  if (childOutOfPluginScope(child)) {
423
450
  continue;
424
451
  }
452
+ if (!isSkillCompatibleWithPlatform(child)) {
453
+ continue;
454
+ }
425
455
  const childFlagKey = skillFlagKey(child);
426
456
  if (
427
457
  childFlagKey &&
@@ -518,10 +548,15 @@ export const skillLoadTool = {
518
548
  childLoaded.skill.directoryPath,
519
549
  );
520
550
  if (childManifest) {
521
- anyChildHasTools = true;
522
- includedBodies.push(
523
- formatToolSchemas(childManifest, childLoaded.skill.displayName),
551
+ const childSchemas = formatToolSchemas(
552
+ childManifest,
553
+ context,
554
+ childLoaded.skill.displayName,
524
555
  );
556
+ if (childSchemas) {
557
+ anyChildHasTools = true;
558
+ includedBodies.push(childSchemas);
559
+ }
525
560
  }
526
561
  }
527
562
  }
@@ -5,6 +5,10 @@ import { join, resolve } from "node:path";
5
5
 
6
6
  import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
7
7
  import { computeSkillVersionHash } from "../../skills/version-hash.js";
8
+ import {
9
+ buildShellInvocation,
10
+ terminateProcessTree,
11
+ } from "../../util/host-process.js";
8
12
  import { safeStringSlice } from "../../util/unicode.js";
9
13
  import { buildSanitizedEnv } from "../terminal/safe-env.js";
10
14
  import type { ToolContext, ToolExecutionResult } from "../types.js";
@@ -139,7 +143,7 @@ function spawnRunner(
139
143
  let timedOut = false;
140
144
 
141
145
  const bunRunCmd = "bun run __skill_runner.ts";
142
- const wrapped = { command: "bash", args: ["-c", "--", bunRunCmd] };
146
+ const wrapped = buildShellInvocation(bunRunCmd);
143
147
 
144
148
  const env = buildSanitizedEnv();
145
149
  env.__SKILL_INPUT_JSON = JSON.stringify(input);
@@ -161,28 +165,16 @@ function spawnRunner(
161
165
 
162
166
  const timer = setTimeout(() => {
163
167
  timedOut = true;
164
- try {
165
- process.kill(-child.pid!, "SIGKILL");
166
- } catch {
167
- // Process group may have already exited.
168
- }
168
+ terminateProcessTree(child);
169
169
  }, timeoutMs);
170
170
 
171
171
  // Cooperative cancellation via AbortSignal
172
172
  const onAbort = () => {
173
- try {
174
- process.kill(-child.pid!, "SIGKILL");
175
- } catch {
176
- // Process group may have already exited.
177
- }
173
+ terminateProcessTree(child);
178
174
  };
179
175
  if (context.signal) {
180
176
  if (context.signal.aborted) {
181
- try {
182
- process.kill(-child.pid!, "SIGKILL");
183
- } catch {
184
- // Process group may have already exited.
185
- }
177
+ terminateProcessTree(child);
186
178
  } else {
187
179
  context.signal.addEventListener("abort", onAbort, { once: true });
188
180
  }
@@ -39,6 +39,7 @@ export function createSkillTool(
39
39
  category: entry.category,
40
40
  defaultRiskLevel: riskMap[entry.risk],
41
41
  executionTarget: entry.execution_target as ExecutionTarget,
42
+ supportedClientOs: entry.supported_client_os,
42
43
 
43
44
  input_schema: entry.input_schema as object,
44
45
 
@@ -1,6 +1,10 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
 
3
- import { buildSanitizedEnv, SAFE_ENV_VARS } from "../safe-env.js";
3
+ import {
4
+ buildSanitizedEnv,
5
+ SAFE_ENV_VARS,
6
+ WINDOWS_SAFE_ENV_VARS,
7
+ } from "../safe-env.js";
4
8
 
5
9
  describe("safe-env Qdrant forwarding", () => {
6
10
  const priorPort = process.env.QDRANT_HTTP_PORT;
@@ -35,3 +39,62 @@ describe("safe-env Qdrant forwarding", () => {
35
39
  expect(env.QDRANT_URL).toBeUndefined();
36
40
  });
37
41
  });
42
+
43
+ describe("safe-env Windows forwarding", () => {
44
+ test("forwards runtime paths only to Windows children", () => {
45
+ const keys = [
46
+ "SystemRoot",
47
+ "COMSPEC",
48
+ "USERPROFILE",
49
+ "LOCALAPPDATA",
50
+ "PATHEXT",
51
+ ] as const;
52
+ const previous = Object.fromEntries(
53
+ keys.map((key) => [key, process.env[key]]),
54
+ );
55
+
56
+ try {
57
+ process.env.SystemRoot = "C:\\Windows";
58
+ process.env.COMSPEC = "C:\\Windows\\System32\\cmd.exe";
59
+ process.env.USERPROFILE = "C:\\Users\\Alice";
60
+ process.env.LOCALAPPDATA = "C:\\Users\\Alice\\AppData\\Local";
61
+ process.env.PATHEXT = ".COM;.EXE;.BAT;.CMD";
62
+
63
+ expect(WINDOWS_SAFE_ENV_VARS).toContain("SystemRoot");
64
+ const windowsEnv = buildSanitizedEnv("win32");
65
+ expect(windowsEnv.SystemRoot).toBe("C:\\Windows");
66
+ expect(windowsEnv.COMSPEC).toBe("C:\\Windows\\System32\\cmd.exe");
67
+ expect(windowsEnv.USERPROFILE).toBe("C:\\Users\\Alice");
68
+ expect(windowsEnv.LOCALAPPDATA).toBe("C:\\Users\\Alice\\AppData\\Local");
69
+ expect(windowsEnv.PATHEXT).toBe(".COM;.EXE;.BAT;.CMD");
70
+
71
+ const posixEnv = buildSanitizedEnv("linux");
72
+ expect(posixEnv.SystemRoot).toBeUndefined();
73
+ expect(posixEnv.COMSPEC).toBeUndefined();
74
+ expect(posixEnv.USERPROFILE).toBeUndefined();
75
+ } finally {
76
+ for (const key of keys) {
77
+ const value = previous[key];
78
+ if (value == null) {
79
+ delete process.env[key];
80
+ } else {
81
+ process.env[key] = value;
82
+ }
83
+ }
84
+ }
85
+ });
86
+
87
+ test("matches Windows environment names case-insensitively", () => {
88
+ const windowsEnv = buildSanitizedEnv("win32", {
89
+ Path: "C:\\Windows\\System32;C:\\Program Files\\Vellum",
90
+ systemroot: "C:\\Windows",
91
+ ComSpec: "C:\\Windows\\System32\\cmd.exe",
92
+ });
93
+
94
+ expect(windowsEnv.PATH).toBe(
95
+ "C:\\Windows\\System32;C:\\Program Files\\Vellum",
96
+ );
97
+ expect(windowsEnv.SystemRoot).toBe("C:\\Windows");
98
+ expect(windowsEnv.COMSPEC).toBe("C:\\Windows\\System32\\cmd.exe");
99
+ });
100
+ });
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import { readdirSync } from "node:fs";
9
9
 
10
+ import { pathListDelimiter } from "@vellumai/environments/shell";
11
+
10
12
  import { getGatewayInternalBaseUrl } from "../../config/env.js";
11
13
  import { getDataDir, getWorkspaceDir } from "../../util/platform.js";
12
14
 
@@ -76,6 +78,18 @@ export const SAFE_ENV_VARS = [
76
78
  "VELLUM_BACKUP_KEY_PATH",
77
79
  ] as const;
78
80
 
81
+ export const WINDOWS_SAFE_ENV_VARS = [
82
+ "SystemRoot",
83
+ "COMSPEC",
84
+ "USERPROFILE",
85
+ "APPDATA",
86
+ "LOCALAPPDATA",
87
+ "TEMP",
88
+ "TMP",
89
+ "PATHEXT",
90
+ "SystemDrive",
91
+ ] as const;
92
+
79
93
  export const KATA_SAFE_ENV_VARS = [
80
94
  "VELLUM_APT_DATA_ROOT",
81
95
  "VELLUM_APT_DATA_SUITE",
@@ -165,26 +179,42 @@ export const ALWAYS_INJECTED_ENV_VARS = [
165
179
  function appendUniquePathEntries(
166
180
  value: string | undefined,
167
181
  entries: readonly string[],
182
+ separator = pathListDelimiter(),
168
183
  ): string {
169
- const parts = value ? value.split(":").filter(Boolean) : [];
184
+ const parts = value ? value.split(separator).filter(Boolean) : [];
170
185
  for (const entry of entries) {
171
186
  if (!parts.includes(entry)) {
172
187
  parts.push(entry);
173
188
  }
174
189
  }
175
- return parts.join(":");
190
+ return parts.join(separator);
176
191
  }
177
192
 
178
- export function buildSanitizedEnv(): Record<string, string> {
193
+ export function buildSanitizedEnv(
194
+ hostPlatform: NodeJS.Platform = process.platform,
195
+ sourceEnv: NodeJS.ProcessEnv = process.env,
196
+ ): Record<string, string> {
179
197
  const env: Record<string, string> = {};
180
- const isKataRuntime = isKataFamilyRuntime(process.env.VELLUM_SANDBOX_RUNTIME);
198
+ const isKataRuntime = isKataFamilyRuntime(sourceEnv.VELLUM_SANDBOX_RUNTIME);
199
+ const platformVars =
200
+ hostPlatform === "win32" ? WINDOWS_SAFE_ENV_VARS : ([] as const);
181
201
  const safeEnvVars = isKataRuntime
182
- ? [...SAFE_ENV_VARS, ...KATA_SAFE_ENV_VARS]
183
- : SAFE_ENV_VARS;
202
+ ? [...SAFE_ENV_VARS, ...platformVars, ...KATA_SAFE_ENV_VARS]
203
+ : [...SAFE_ENV_VARS, ...platformVars];
184
204
 
205
+ const windowsEnv =
206
+ hostPlatform === "win32"
207
+ ? new Map(
208
+ Object.entries(sourceEnv).map(([key, value]) => [
209
+ key.toLowerCase(),
210
+ value,
211
+ ]),
212
+ )
213
+ : null;
185
214
  for (const key of safeEnvVars) {
186
- if (process.env[key] != null) {
187
- env[key] = process.env[key]!;
215
+ const value = windowsEnv?.get(key.toLowerCase()) ?? sourceEnv[key];
216
+ if (value != null) {
217
+ env[key] = value;
188
218
  }
189
219
  }
190
220
  if (isKataRuntime) {
@@ -207,7 +237,7 @@ export function buildSanitizedEnv(): Record<string, string> {
207
237
  env.BUN_INSTALL = `${env.HOME}/.bun`;
208
238
  env.PATH = appendUniquePathEntries(
209
239
  `${env.PYTHONUSERBASE}/bin:${env.BUN_INSTALL}/bin`,
210
- env.PATH.split(":").filter(Boolean),
240
+ env.PATH.split(pathListDelimiter()).filter(Boolean),
211
241
  );
212
242
  }
213
243
  }
@@ -1,4 +1,3 @@
1
- import type { ChildProcess } from "node:child_process";
2
1
  import { spawn } from "node:child_process";
3
2
 
4
3
  import { z } from "zod";
@@ -10,6 +9,10 @@ import { wakeAgentForOpportunity } from "../../runtime/agent-wake.js";
10
9
  import { broadcastMessage } from "../../runtime/assistant-event-hub.js";
11
10
  import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
12
11
  import { redactSecrets } from "../../security/secret-scanner.js";
12
+ import {
13
+ buildShellInvocation,
14
+ terminateProcessTree,
15
+ } from "../../util/host-process.js";
13
16
  import { getLogger } from "../../util/logger.js";
14
17
  import { getDataDir } from "../../util/platform.js";
15
18
  import type { CompletedBackgroundTool } from "../background-tool-registry.js";
@@ -296,7 +299,7 @@ export const shellTool = {
296
299
  Object.assign(env, proxyEnv);
297
300
  }
298
301
 
299
- const wrapped = { command: "bash", args: ["-c", "--", command] };
302
+ const wrapped = buildShellInvocation(command);
300
303
 
301
304
  // -----------------------------------------------------------------------
302
305
  // Background mode: spawn and return immediately. The process output is
@@ -644,12 +647,11 @@ export const shellTool = {
644
647
 
645
648
  /**
646
649
  * Structured teardown log. Pairs with the `"Executing shell command"`
647
- * start log: every shell invocation now produces a start/exit pair so
650
+ * start log: every shell invocation produces a start/exit pair so
648
651
  * orphan-leak post-mortems can correlate command + exitCode + signal +
649
652
  * timedOut + duration without spelunking through prose hints. The
650
- * `signal === "SIGKILL"` + `timedOut === true` combination is the
651
- * fingerprint left by the timeout watcher SIGKILLing the process group
652
- * — i.e. the moment that creates the orphans.
653
+ * `signal === "SIGKILL"` + `timedOut === true` combination identifies a
654
+ * timeout-driven process termination.
653
655
  */
654
656
  function logShellExit(args: {
655
657
  toolName: string;
@@ -681,19 +683,10 @@ function logShellExit(args: {
681
683
  }
682
684
 
683
685
  /**
684
- * Kill the entire process tree of a child process. Tries the process group
685
- * first (negative PID), then falls back to killing the direct child if the
686
- * PID is unavailable or the group kill fails.
687
- *
688
- * Emits a structured `warn` log on every invocation: this is the
689
- * ground-truth event that creates orphaned subprocesses (the SIGKILL hits
690
- * the entire group, so the immediate bash child has no chance to reap its
691
- * grandchildren; under bun-as-PID-1 they accumulate as `<defunct>`).
692
- * `reason` lets the next zombie report point at a specific call site (the
693
- * timeout watcher in the foreground/background branches, or an abort).
686
+ * Terminate the process tree and record the call site for timeout diagnostics.
694
687
  */
695
688
  function buildKillTree(
696
- child: ChildProcess,
689
+ child: ReturnType<typeof spawn>,
697
690
  context: {
698
691
  toolName: string;
699
692
  conversationId: string;
@@ -715,20 +708,8 @@ function buildKillTree(
715
708
  groupPid,
716
709
  invocationId: context.invocationId,
717
710
  },
718
- "Shell process group SIGKILL'd — orphans expected to reparent to PID 1",
711
+ "Shell process group SIGKILL'd",
719
712
  );
720
- if (groupPid != null) {
721
- try {
722
- process.kill(-groupPid, "SIGKILL");
723
- return;
724
- } catch {
725
- // Process group may have already exited — fall through.
726
- }
727
- }
728
- try {
729
- child.kill("SIGKILL");
730
- } catch {
731
- // Child may have already exited.
732
- }
713
+ terminateProcessTree(child);
733
714
  };
734
715
  }