@vellumai/assistant 0.11.7 → 0.11.8-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 (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -1,6 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { resolveWorkerCommand } from "../worker-process.js";
3
+ import {
4
+ classifyOrphanAfterWait,
5
+ decideWorkerSlot,
6
+ resolveWorkerCommand,
7
+ workerKindSignature,
8
+ type WorkerProcessStatus,
9
+ } from "../worker-process.js";
4
10
 
5
11
  const entry = new URL("file:///source/monitoring/worker.ts");
6
12
 
@@ -35,3 +41,242 @@ describe("resolveWorkerCommand", () => {
35
41
  ).toEqual(["bun", "--smol", "run", "/source/monitoring/worker.ts"]);
36
42
  });
37
43
  });
44
+
45
+ describe("workerKindSignature", () => {
46
+ const schedule = new URL(
47
+ "file:////app/runtime/0.11.7/src/schedule/worker.ts",
48
+ );
49
+ const source = {
50
+ platform: "darwin" as const,
51
+ execPath: "/runtime/bun",
52
+ executableExists: () => false,
53
+ };
54
+
55
+ test("is the same for a worker of this kind from any install", () => {
56
+ const previous = new URL(
57
+ "file:////app/runtime/0.10.11/src/schedule/worker.ts",
58
+ );
59
+ expect(workerKindSignature(schedule, "schedule", source)).toEqual(
60
+ workerKindSignature(previous, "schedule", source),
61
+ );
62
+ });
63
+
64
+ test("distinguishes one worker kind from another", () => {
65
+ const monitoring = new URL(
66
+ "file:////app/runtime/0.11.7/src/monitoring/worker.ts",
67
+ );
68
+ expect(workerKindSignature(schedule, "schedule", source)).not.toEqual(
69
+ workerKindSignature(monitoring, "monitoring", source),
70
+ );
71
+ });
72
+
73
+ // The signature only guards against signalling a recycled PID, so it has to
74
+ // be specific enough that an unrelated program running its own worker.ts
75
+ // never matches.
76
+ const matches = (command: string, signature: readonly string[]): boolean =>
77
+ signature.every((part) => command.includes(part));
78
+
79
+ test("does not match an unrelated program running some other worker.ts", () => {
80
+ const signature = workerKindSignature(schedule, "schedule", source);
81
+ expect(matches("bun run /home/dev/side-project/worker.ts", signature)).toBe(
82
+ false,
83
+ );
84
+ expect(matches("node /srv/queue/src/jobs/worker.ts", signature)).toBe(
85
+ false,
86
+ );
87
+ });
88
+
89
+ test("matches the same worker from a previous install", () => {
90
+ const signature = workerKindSignature(schedule, "schedule", source);
91
+ expect(
92
+ matches(
93
+ "bun --smol run /app/runtime/0.10.11/src/schedule/worker.ts",
94
+ signature,
95
+ ),
96
+ ).toBe(true);
97
+ });
98
+
99
+ const packaged = {
100
+ platform: "win32" as const,
101
+ execPath: "/runtime/vellum-daemon.exe",
102
+ executableExists: () => true,
103
+ };
104
+
105
+ // Every packaged worker runs one executable and is told apart by the
106
+ // subcommand, so the executable alone must not identify a worker: one
107
+ // worker's slot would otherwise reclaim another's process.
108
+ test("distinguishes packaged worker kinds sharing one executable", () => {
109
+ const scheduleSig = workerKindSignature(schedule, "schedule", packaged);
110
+ expect(matches('"C:/App/vellum-worker.exe" schedule', scheduleSig)).toBe(
111
+ true,
112
+ );
113
+ expect(matches('"C:/App/vellum-worker.exe" monitoring', scheduleSig)).toBe(
114
+ false,
115
+ );
116
+ });
117
+
118
+ test("matches a packaged worker from a previous install", () => {
119
+ const scheduleSig = workerKindSignature(schedule, "schedule", packaged);
120
+ expect(matches('"C:/Prev/vellum-worker.exe" schedule', scheduleSig)).toBe(
121
+ true,
122
+ );
123
+ });
124
+ });
125
+
126
+ // Every branch that can reach a kill. `reclaim` is the only decision that
127
+ // signals a process, so each test below is really asking: could this input
128
+ // have killed something it should not have?
129
+ describe("decideWorkerSlot", () => {
130
+ const DAEMON = 900;
131
+ const WORKER = 4242;
132
+ const signature = ["src/schedule/worker.ts"];
133
+ const running = { status: "running" as const, pid: WORKER };
134
+ const ours = `bun --smol run /app/runtime/0.11.7/src/schedule/worker.ts`;
135
+ const previous = `bun --smol run /app/runtime/0.10.11/src/schedule/worker.ts`;
136
+ const alive = () => true;
137
+ const dead = () => false;
138
+
139
+ const decide = (
140
+ row: { pid: number; ppid: number; command: string } | null,
141
+ isOwnerAlive = alive,
142
+ pid1OwnsWorkers = false,
143
+ status: WorkerProcessStatus = running,
144
+ ) =>
145
+ decideWorkerSlot(
146
+ status,
147
+ row,
148
+ signature,
149
+ DAEMON,
150
+ isOwnerAlive,
151
+ pid1OwnsWorkers,
152
+ );
153
+
154
+ test("spawns when the PID file names nothing running", () => {
155
+ expect(decide(null, alive, false, { status: "not_running" })).toEqual({
156
+ action: "spawn",
157
+ });
158
+ });
159
+
160
+ test("adopts this daemon's own worker", () => {
161
+ expect(decide({ pid: WORKER, ppid: DAEMON, command: ours })).toEqual({
162
+ action: "adopt",
163
+ pid: WORKER,
164
+ });
165
+ });
166
+
167
+ test("reclaims a worker reparented to init after its daemon died", () => {
168
+ expect(decide({ pid: WORKER, ppid: 1, command: previous })).toEqual({
169
+ action: "reclaim",
170
+ pid: WORKER,
171
+ });
172
+ });
173
+
174
+ test("reclaims a worker whose owner is gone", () => {
175
+ expect(decide({ pid: WORKER, ppid: 777, command: previous }, dead)).toEqual(
176
+ {
177
+ action: "reclaim",
178
+ pid: WORKER,
179
+ },
180
+ );
181
+ });
182
+
183
+ // `isOwnerAlive` is "is the owner a daemon", not "does that PID exist". These
184
+ // workers have exactly one legitimate owner, so a recycled owner PID must not
185
+ // keep a stranded worker looking owned.
186
+ test("adopts a worker a live daemon owns", () => {
187
+ expect(decide({ pid: WORKER, ppid: 777, command: ours }, alive)).toEqual({
188
+ action: "adopt",
189
+ pid: WORKER,
190
+ });
191
+ });
192
+
193
+ test("reclaims a worker whose owner PID was recycled to a non-daemon", () => {
194
+ const ownerIsNotADaemon = () => false;
195
+ expect(
196
+ decide({ pid: WORKER, ppid: 777, command: previous }, ownerIsNotADaemon),
197
+ ).toEqual({ action: "reclaim", pid: WORKER });
198
+ });
199
+
200
+ // In a container the daemon is PID 1, so a worker parented to 1 is a live
201
+ // sibling's, not an orphan.
202
+ test("adopts a PID-1 child when PID 1 is the daemon", () => {
203
+ expect(
204
+ decide({ pid: WORKER, ppid: 1, command: ours }, alive, true),
205
+ ).toEqual({ action: "adopt", pid: WORKER });
206
+ });
207
+
208
+ // The cases below are the ones that must never reach a kill.
209
+ test("never signals an unrelated process on a recycled PID", () => {
210
+ expect(
211
+ decide({ pid: WORKER, ppid: 1, command: "/usr/bin/postgres -D /data" }),
212
+ ).toEqual({ action: "adopt", pid: WORKER });
213
+ });
214
+
215
+ test("never signals another project's worker.ts on a recycled PID", () => {
216
+ expect(
217
+ decide({
218
+ pid: WORKER,
219
+ ppid: 1,
220
+ command: "bun run /home/dev/side-project/worker.ts",
221
+ }),
222
+ ).toEqual({ action: "adopt", pid: WORKER });
223
+ });
224
+
225
+ test("never signals a different worker kind holding this slot", () => {
226
+ expect(
227
+ decide({
228
+ pid: WORKER,
229
+ ppid: 1,
230
+ command: "bun --smol run /app/runtime/0.10.11/src/monitoring/worker.ts",
231
+ }),
232
+ ).toEqual({ action: "adopt", pid: WORKER });
233
+ });
234
+
235
+ test("never signals when the process table could not be read", () => {
236
+ expect(decide(null)).toEqual({ action: "adopt", pid: WORKER });
237
+ });
238
+ });
239
+
240
+ // Escalation happens after an awaited SIGTERM grace, so the PID alone cannot
241
+ // carry identity across it: the orphan may exit mid-wait and the OS may hand
242
+ // its PID to a stranger, or the process table may simply fail to answer.
243
+ // Only a positive re-match escalates, and only a proven "gone" releases the
244
+ // slot; a live PID whose identity cannot be read resolves to reuse, because
245
+ // releasing it would spawn a second worker next to a live one.
246
+ describe("classifyOrphanAfterWait", () => {
247
+ const signature = ["src/schedule/worker.ts"];
248
+ const ours = "bun --smol run /app/runtime/0.10.11/src/schedule/worker.ts";
249
+
250
+ test("dead PID: gone, slot may be released", () => {
251
+ expect(classifyOrphanAfterWait(false, null, signature)).toBe("gone");
252
+ expect(classifyOrphanAfterWait(false, ours, signature)).toBe("gone");
253
+ });
254
+
255
+ test("still this worker: escalation proceeds", () => {
256
+ expect(classifyOrphanAfterWait(true, ours, signature)).toBe("orphan");
257
+ });
258
+
259
+ test("PID reused by a stranger: orphan exited, never SIGKILL", () => {
260
+ expect(
261
+ classifyOrphanAfterWait(true, "/usr/bin/postgres -D /data", signature),
262
+ ).toBe("gone");
263
+ });
264
+
265
+ test("PID reused by a different worker kind: never SIGKILL", () => {
266
+ expect(
267
+ classifyOrphanAfterWait(
268
+ true,
269
+ "bun --smol run /app/runtime/0.10.11/src/monitoring/worker.ts",
270
+ signature,
271
+ ),
272
+ ).toBe("gone");
273
+ });
274
+
275
+ // The fail-safe vex pinned: a live PID with an unreadable command line is
276
+ // neither killable nor releasable.
277
+ test("alive but command unreadable: identity-unreadable, reuse the slot", () => {
278
+ expect(classifyOrphanAfterWait(true, null, signature)).toBe(
279
+ "identity-unreadable",
280
+ );
281
+ });
282
+ });
@@ -22,7 +22,7 @@ import { arch, homedir, platform } from "node:os";
22
22
  import { basename, join } from "node:path";
23
23
 
24
24
  import { getLogger } from "./logger.js";
25
- import { getBinDir } from "./platform.js";
25
+ import { getBinDir, isWindows } from "./platform.js";
26
26
 
27
27
  const log = getLogger("bun-runtime");
28
28
 
@@ -81,19 +81,31 @@ export function findBun(): string | undefined {
81
81
  return process.execPath;
82
82
  }
83
83
 
84
+ const exe = isWindows() ? "bun.exe" : "bun";
85
+
84
86
  // 2. Previously downloaded copy
85
- const downloaded = join(getBinDir(), "bun");
87
+ const downloaded = join(getBinDir(), exe);
86
88
  if (existsSync(downloaded)) {
87
89
  return downloaded;
88
90
  }
89
91
 
90
92
  // 3. Common install locations
91
93
  const home = homedir();
92
- for (const p of [
93
- join(home, ".bun", "bin", "bun"),
94
- "/opt/homebrew/bin/bun",
95
- "/usr/local/bin/bun",
96
- ]) {
94
+ const candidates = isWindows()
95
+ ? [
96
+ join(home, ".bun", "bin", exe),
97
+ join(
98
+ process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"),
99
+ "bun",
100
+ exe,
101
+ ),
102
+ ]
103
+ : [
104
+ join(home, ".bun", "bin", exe),
105
+ "/opt/homebrew/bin/bun",
106
+ "/usr/local/bin/bun",
107
+ ];
108
+ for (const p of candidates) {
97
109
  if (existsSync(p)) {
98
110
  return p;
99
111
  }
@@ -108,13 +120,20 @@ export function findBun(): string | undefined {
108
120
  return undefined;
109
121
  }
110
122
 
123
+ /** Escape a string for a PowerShell single-quoted literal. */
124
+ function psQuote(value: string): string {
125
+ return value.replace(/'/g, "''");
126
+ }
127
+
111
128
  /**
112
129
  * Download a pinned bun release into the given directory.
113
130
  * Returns the absolute path to the downloaded binary.
114
131
  */
115
132
  async function downloadBun(installDir: string): Promise<string> {
116
- const os = platform();
117
- const cpu = arch() === "arm64" ? "aarch64" : arch();
133
+ const win = isWindows();
134
+ const os = win ? "windows" : platform();
135
+ // Bun ships no Windows ARM64 build; the x64 one runs under emulation.
136
+ const cpu = win ? "x64" : arch() === "arm64" ? "aarch64" : arch();
118
137
  const target = `${os}-${cpu}`;
119
138
  const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-${target}.zip`;
120
139
 
@@ -135,9 +154,18 @@ async function downloadBun(installDir: string): Promise<string> {
135
154
 
136
155
  try {
137
156
  const proc = Bun.spawn({
138
- cmd: ["unzip", "-o", tmpZip, "-d", installDir],
157
+ cmd: win
158
+ ? [
159
+ "powershell.exe",
160
+ "-NoProfile",
161
+ "-NonInteractive",
162
+ "-Command",
163
+ `Expand-Archive -LiteralPath '${psQuote(tmpZip)}' -DestinationPath '${psQuote(installDir)}' -Force`,
164
+ ]
165
+ : ["unzip", "-o", tmpZip, "-d", installDir],
139
166
  stdout: "ignore",
140
167
  stderr: "pipe",
168
+ windowsHide: true,
141
169
  });
142
170
  await proc.exited;
143
171
  if (proc.exitCode !== 0) {
@@ -146,8 +174,9 @@ async function downloadBun(installDir: string): Promise<string> {
146
174
  }
147
175
 
148
176
  // Move binary from bun-{target}/bun to installDir/bun
149
- const extractedBun = join(installDir, `bun-${target}`, "bun");
150
- const finalPath = join(installDir, "bun");
177
+ const exe = win ? "bun.exe" : "bun";
178
+ const extractedBun = join(installDir, `bun-${target}`, exe);
179
+ const finalPath = join(installDir, exe);
151
180
  if (existsSync(extractedBun)) {
152
181
  // Remove old binary first to avoid "Text file busy" on some systems
153
182
  if (existsSync(finalPath)) {
@@ -164,7 +193,9 @@ async function downloadBun(installDir: string): Promise<string> {
164
193
  );
165
194
  }
166
195
 
167
- chmodSync(finalPath, 0o755);
196
+ if (!win) {
197
+ chmodSync(finalPath, 0o755);
198
+ }
168
199
 
169
200
  log.info({ path: finalPath }, "Bun binary downloaded successfully");
170
201
  return finalPath;
@@ -1,6 +1,14 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
+ export function isBunVirtualPath(value: string): boolean {
5
+ const normalized = value.replaceAll("\\", "/");
6
+ return (
7
+ normalized.startsWith("/$bunfs/") ||
8
+ /^[a-z]:\/~bun(?:\/|$)/i.test(normalized)
9
+ );
10
+ }
11
+
4
12
  /**
5
13
  * Resolve the path to a bundled asset directory, handling compiled Bun binaries
6
14
  * where `import.meta.dirname` points to the `/$bunfs/` virtual filesystem and
@@ -22,7 +30,7 @@ export function resolveBundledDir(
22
30
  relativePath: string,
23
31
  bundleName: string,
24
32
  ): string {
25
- if (callerDir.startsWith("/$bunfs/")) {
33
+ if (isBunVirtualPath(callerDir)) {
26
34
  const execDir = dirname(process.execPath);
27
35
  // macOS .app bundle: binary in Contents/MacOS/, resources in Contents/Resources/
28
36
  const resourcesPath = join(execDir, "..", "Resources", bundleName);
@@ -30,6 +30,7 @@ async function getDirectorySizeBytes(paths: string[]): Promise<number | null> {
30
30
  }
31
31
  const { stdout } = await execFileAsync("du", ["-sb", ...existing], {
32
32
  timeout: 30_000,
33
+ windowsHide: true,
33
34
  });
34
35
  let total = 0;
35
36
  for (const line of stdout.trim().split("\n")) {
@@ -48,7 +48,10 @@ type ExecFileRunner = (
48
48
  ) => Promise<ExecFileResult>;
49
49
 
50
50
  const runExecFile: ExecFileRunner = async (command, args, options) => {
51
- const { stdout } = await execFileAsync(command, args, options);
51
+ const { stdout } = await execFileAsync(command, args, {
52
+ ...options,
53
+ windowsHide: true,
54
+ });
52
55
  return { stdout };
53
56
  };
54
57
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Timings for the forms the guardian fills in their app, shared by the CLI
3
+ * that waits and the registry that holds the form open.
4
+ *
5
+ * One budget split across a socket and a timer, so the numbers live together
6
+ * and the caller's wait is derived from the deadlines rather than written down
7
+ * beside them. The invariant: the caller outlasts the form. A caller that
8
+ * gives up first reports a failure while the write it was waiting on goes on
9
+ * to commit.
10
+ *
11
+ * Under `util/` because the CLI hoists it: constants and one pure function,
12
+ * with no daemon runtime graph behind it (`cli/no-daemon-internals`).
13
+ */
14
+
15
+ /** Default time a form stays open for an answer (5 min). */
16
+ export const GUARDIAN_FORM_DEFAULT_TIMEOUT_MS = 300_000;
17
+
18
+ /**
19
+ * Ceiling on a caller-supplied wait (1 hour), so a bad value cannot park a
20
+ * pending form indefinitely.
21
+ */
22
+ export const GUARDIAN_FORM_MAX_TIMEOUT_MS = 3_600_000;
23
+
24
+ /**
25
+ * How long a claimed form is held while its write settles (3 min).
26
+ *
27
+ * A claim means somebody answered, so the open-for-answers deadline stops
28
+ * governing and this one takes over. It covers the gateway's whole write
29
+ * rather than one call of it: its IPC calls bound at 30s each and a delete
30
+ * makes three in sequence (mirror probe, mirror delete, then the resolve
31
+ * back).
32
+ */
33
+ export const GUARDIAN_FORM_SETTLE_MS = 180_000;
34
+
35
+ /** Slack on top of the deadlines, so the socket is never the first to give up. */
36
+ const GUARDIAN_FORM_TRANSPORT_BUFFER_MS = 10_000;
37
+
38
+ /**
39
+ * How long a caller waits on a form that stays open for `timeoutMs`.
40
+ *
41
+ * Covers the answer window plus the settle window, since an answer landing at
42
+ * the deadline starts the settle window from there. A caller that gives up
43
+ * first reports a failure while the write proceeds, which for a delete means a
44
+ * contact removed against a command that reported nothing happened.
45
+ */
46
+ export function guardianFormCallBudgetMs(timeoutMs: number): number {
47
+ return (
48
+ timeoutMs + GUARDIAN_FORM_SETTLE_MS + GUARDIAN_FORM_TRANSPORT_BUFFER_MS
49
+ );
50
+ }
@@ -260,6 +260,7 @@ async function runSips(
260
260
  stdout: "ignore",
261
261
  stderr: "ignore",
262
262
  timeout: 15_000,
263
+ windowsHide: true,
263
264
  });
264
265
  await proc.exited;
265
266
  if (proc.exitCode !== 0) {
@@ -2,7 +2,9 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  findContentTypeHeader,
5
+ isBinaryOAuthContentType,
5
6
  isJsonContentType,
7
+ parseRequestBodyBytes,
6
8
  parseRequestBodyData,
7
9
  } from "./oauth-request-body.js";
8
10
 
@@ -72,3 +74,60 @@ describe("parseRequestBodyData", () => {
72
74
  expect(parseRequestBodyData('"hello"', undefined)).toBe('"hello"');
73
75
  });
74
76
  });
77
+
78
+ describe("isBinaryOAuthContentType", () => {
79
+ test("recognizes PDF, octet-stream, and image types", () => {
80
+ expect(isBinaryOAuthContentType("application/pdf")).toBe(true);
81
+ expect(isBinaryOAuthContentType("application/octet-stream")).toBe(true);
82
+ expect(isBinaryOAuthContentType("image/png; charset=binary")).toBe(true);
83
+ });
84
+
85
+ test("rejects JSON, multipart, and form types", () => {
86
+ expect(isBinaryOAuthContentType("application/json")).toBe(false);
87
+ expect(isBinaryOAuthContentType("multipart/related; boundary=b")).toBe(
88
+ false,
89
+ );
90
+ expect(isBinaryOAuthContentType("application/x-www-form-urlencoded")).toBe(
91
+ false,
92
+ );
93
+ expect(isBinaryOAuthContentType(undefined)).toBe(false);
94
+ });
95
+ });
96
+
97
+ describe("parseRequestBodyBytes", () => {
98
+ const png = Buffer.from([
99
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe,
100
+ ]);
101
+
102
+ test("keeps invalid UTF-8 as a Buffer", () => {
103
+ const parsed = parseRequestBodyBytes(png, "multipart/related; boundary=b");
104
+ expect(Buffer.isBuffer(parsed)).toBe(true);
105
+ expect(Buffer.from(parsed as Uint8Array).equals(png)).toBe(true);
106
+ });
107
+
108
+ test("keeps a PDF Content-Type as a Buffer even when the bytes are ASCII", () => {
109
+ const asciiPdf = Buffer.from("%PDF-1.4 simple", "ascii");
110
+ const parsed = parseRequestBodyBytes(asciiPdf, "application/pdf");
111
+ expect(Buffer.isBuffer(parsed)).toBe(true);
112
+ expect(Buffer.from(parsed as Uint8Array).equals(asciiPdf)).toBe(true);
113
+ });
114
+
115
+ test("keeps valid UTF-8 multipart as the exact string", () => {
116
+ const multipart = "--b\r\nContent-Type: text/csv\r\n\r\na,b\r\n--b--\r\n";
117
+ expect(
118
+ parseRequestBodyBytes(
119
+ Buffer.from(multipart, "utf-8"),
120
+ "multipart/related; boundary=b",
121
+ ),
122
+ ).toBe(multipart);
123
+ });
124
+
125
+ test("parses UTF-8 JSON into an object", () => {
126
+ expect(
127
+ parseRequestBodyBytes(
128
+ Buffer.from('{"name":"Sheet"}', "utf-8"),
129
+ "application/json",
130
+ ),
131
+ ).toEqual({ name: "Sheet" });
132
+ });
133
+ });
@@ -1,13 +1,23 @@
1
1
  /**
2
2
  * Shared helpers for deciding how an outbound OAuth request body is encoded.
3
3
  *
4
- * A body is either structured (an object or array, serialized as JSON) or
5
- * raw (a string forwarded to the provider byte-for-byte). The Content-Type
6
- * the caller supplied decides which one a string body is: multipart, XML,
7
- * form-encoded, and CSV payloads must survive intact, while JSON text is
8
- * already in its wire form and must not be re-encoded.
4
+ * A body is structured (an object or array, serialized as JSON), raw text
5
+ * (a string forwarded to the provider as UTF-8), or raw bytes (a Buffer
6
+ * forwarded to the provider byte-for-byte). The Content-Type the caller
7
+ * supplied decides which one a string body is: multipart, XML, form-encoded,
8
+ * and CSV payloads must survive intact, while JSON text is already in its
9
+ * wire form and must not be re-encoded. Files that are not valid UTF-8, or
10
+ * that carry a binary Content-Type, stay as bytes.
9
11
  */
10
12
 
13
+ const BINARY_MEDIA_TYPES = new Set([
14
+ "application/octet-stream",
15
+ "application/pdf",
16
+ "application/zip",
17
+ "application/gzip",
18
+ "application/x-gzip",
19
+ ]);
20
+
11
21
  /** True when a Content-Type header names a JSON media type. */
12
22
  export function isJsonContentType(
13
23
  contentType: string | undefined | null,
@@ -58,3 +68,44 @@ export function parseRequestBodyData(
58
68
  // body once unquoted, so the original quoted text is kept as the wire form.
59
69
  return typeof parsed === "string" ? raw : parsed;
60
70
  }
71
+
72
+ /** True when a Content-Type names a binary media type (PDF, image, zip). */
73
+ export function isBinaryOAuthContentType(
74
+ contentType: string | undefined | null,
75
+ ): boolean {
76
+ if (!contentType) {
77
+ return false;
78
+ }
79
+ const mediaType = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
80
+ if (
81
+ mediaType.startsWith("image/") ||
82
+ mediaType.startsWith("audio/") ||
83
+ mediaType.startsWith("video/")
84
+ ) {
85
+ return true;
86
+ }
87
+ return BINARY_MEDIA_TYPES.has(mediaType);
88
+ }
89
+
90
+ /**
91
+ * Decide how file or stdin bytes reach the provider.
92
+ *
93
+ * Binary Content-Types and payloads that are not valid UTF-8 stay as a
94
+ * Buffer so the original bytes survive the JSON proxy envelope. Valid UTF-8
95
+ * follows {@link parseRequestBodyData}.
96
+ */
97
+ export function parseRequestBodyBytes(
98
+ raw: Uint8Array,
99
+ contentType: string | undefined,
100
+ ): unknown {
101
+ if (isBinaryOAuthContentType(contentType)) {
102
+ return Buffer.from(raw);
103
+ }
104
+ let utf8: string;
105
+ try {
106
+ utf8 = new TextDecoder("utf-8", { fatal: true }).decode(raw);
107
+ } catch {
108
+ return Buffer.from(raw);
109
+ }
110
+ return parseRequestBodyData(utf8, contentType);
111
+ }