@vellumai/assistant 0.3.4 → 0.3.6

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 (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -2,6 +2,7 @@ import { and, asc, desc, eq, lte } from 'drizzle-orm';
2
2
  import { v4 as uuid } from 'uuid';
3
3
  import { Cron } from 'croner';
4
4
  import { getDb } from '../memory/db.js';
5
+ import { rawChanges } from '../memory/raw-query.js';
5
6
  import { scheduleJobs, scheduleRuns } from '../memory/schema.js';
6
7
  import { computeNextRunAt as computeNextRunAtEngine, isValidScheduleExpression } from './recurrence-engine.js';
7
8
  import { getLogger } from '../util/logger.js';
@@ -189,8 +190,8 @@ export function updateSchedule(
189
190
 
190
191
  export function deleteSchedule(id: string): boolean {
191
192
  const db = getDb();
192
- const result = db.delete(scheduleJobs).where(eq(scheduleJobs.id, id)).run() as unknown as { changes?: number };
193
- return (result.changes ?? 0) > 0;
193
+ db.delete(scheduleJobs).where(eq(scheduleJobs.id, id)).run();
194
+ return rawChanges() > 0;
194
195
  }
195
196
 
196
197
  /**
@@ -244,13 +245,13 @@ export function claimDueSchedules(now: number): ScheduleJob[] {
244
245
  updates.nextRunAt = newNextRunAt!;
245
246
  }
246
247
 
247
- const result = db
248
+ db
248
249
  .update(scheduleJobs)
249
250
  .set(updates)
250
251
  .where(and(eq(scheduleJobs.id, row.id), eq(scheduleJobs.nextRunAt, row.nextRunAt)))
251
- .run() as unknown as { changes?: number };
252
+ .run();
252
253
 
253
- if ((result.changes ?? 0) === 0) continue;
254
+ if (rawChanges() === 0) continue;
254
255
 
255
256
  claimed.push(parseJobRow({
256
257
  ...row,
@@ -359,7 +360,14 @@ export function formatLocalDate(timestamp: number): string {
359
360
  export function describeCronExpression(expr: string): string {
360
361
  try {
361
362
  const cron = new Cron(expr, { maxRuns: 0 });
362
- const p = (cron as unknown as { _states: { pattern: {
363
+ // Access Croner internal state to extract the parsed cron pattern.
364
+ // This is fragile but necessary — Croner doesn't expose a public API for this.
365
+ const cronInternal = cron as unknown as Record<string, unknown>;
366
+ const states = cronInternal._states;
367
+ if (!states || typeof states !== 'object') return expr;
368
+ const p = (states as Record<string, unknown>).pattern;
369
+ if (!p || typeof p !== 'object') return expr;
370
+ const pattern = p as {
363
371
  minute: number[];
364
372
  hour: number[];
365
373
  day: number[];
@@ -367,20 +375,28 @@ export function describeCronExpression(expr: string): string {
367
375
  dayOfWeek: number[];
368
376
  starDOM: boolean;
369
377
  starDOW: boolean;
370
- } } })._states.pattern;
378
+ };
371
379
 
372
- const activeMinutes = p.minute.reduce<number[]>((acc, v, i) => { if (v) acc.push(i); return acc; }, []);
373
- const activeHours = p.hour.reduce<number[]>((acc, v, i) => { if (v) acc.push(i); return acc; }, []);
374
- const activeDays = p.day.reduce<number[]>((acc, v, i) => { if (v) acc.push(i + 1); return acc; }, []);
375
- const activeDOW = p.dayOfWeek.reduce<number[]>((acc, v, i) => { if (v) acc.push(i); return acc; }, []);
376
- const activeMonths = p.month.reduce<number[]>((acc, v, i) => { if (v) acc.push(i + 1); return acc; }, []);
380
+ const activeMinutes = pattern.minute.reduce<number[]>((acc, v, i) => { if (v) acc.push(i); return acc; }, []);
381
+ const activeHours = pattern.hour.reduce<number[]>((acc, v, i) => { if (v) acc.push(i); return acc; }, []);
382
+ const activeDays = pattern.day.reduce<number[]>((acc, v, i) => { if (v) acc.push(i + 1); return acc; }, []);
383
+ const activeDOW = pattern.dayOfWeek.reduce<number[]>((acc, v, i) => { if (v) acc.push(i); return acc; }, []);
384
+ const activeMonths = pattern.month.reduce<number[]>((acc, v, i) => { if (v) acc.push(i + 1); return acc; }, []);
377
385
 
378
386
  const allMinutes = activeMinutes.length === 60;
379
387
  const allHours = activeHours.length === 24;
380
- const allDays = p.starDOM;
381
- const allDOW = p.starDOW;
388
+ const allDays = pattern.starDOM;
389
+ const allDOW = pattern.starDOW;
382
390
  const allMonths = activeMonths.length === 12;
383
391
 
392
+ const fixedMinute = activeMinutes.length === 1;
393
+ const fixedHour = activeHours.length === 1;
394
+ const fixedTime = fixedMinute && fixedHour;
395
+ const steppedMinutes = !allMinutes && activeMinutes.length > 1;
396
+ const steppedHours = !allHours && activeHours.length > 1;
397
+ const anyDay = allDays && allDOW;
398
+ const anyDayAndMonth = anyDay && allMonths;
399
+
384
400
  // Format time as 12-hour clock
385
401
  function formatTime(hour: number, minute: number): string {
386
402
  const period = hour >= 12 ? 'PM' : 'AM';
@@ -396,14 +412,11 @@ export function describeCronExpression(expr: string): string {
396
412
  return n + (s[(v - 20) % 10] || s[v] || s[0]);
397
413
  }
398
414
 
399
- // Every minute: all fields are wildcard
400
- if (allMinutes && allHours && allDays && allDOW && allMonths) {
415
+ if (allMinutes && allHours && anyDayAndMonth) {
401
416
  return 'Every minute';
402
417
  }
403
418
 
404
- // Every N minutes: multiple minutes, all hours, all days
405
- if (!allMinutes && activeMinutes.length > 1 && allHours && allDays && allDOW && allMonths) {
406
- // Check if it's a regular step pattern (e.g. */5)
419
+ if (steppedMinutes && allHours && anyDayAndMonth) {
407
420
  if (activeMinutes.length >= 2 && activeMinutes[0] === 0) {
408
421
  const step = activeMinutes[1] - activeMinutes[0];
409
422
  const isRegularStep = activeMinutes.every((v, i) => v === i * step);
@@ -413,16 +426,14 @@ export function describeCronExpression(expr: string): string {
413
426
  }
414
427
  }
415
428
 
416
- // Every hour: minute is fixed at one value, all hours, all days
417
- if (activeMinutes.length === 1 && allHours && allDays && allDOW && allMonths) {
429
+ if (fixedMinute && allHours && anyDayAndMonth) {
418
430
  if (activeMinutes[0] === 0) {
419
431
  return 'Every hour';
420
432
  }
421
433
  return `Every hour at minute ${activeMinutes[0]}`;
422
434
  }
423
435
 
424
- // Every N hours: minute is fixed, multiple hours with regular stepping, all days
425
- if (activeMinutes.length === 1 && !allHours && activeHours.length > 1 && allDays && allDOW && allMonths) {
436
+ if (fixedMinute && steppedHours && anyDayAndMonth) {
426
437
  if (activeHours.length >= 2 && activeHours[0] === 0) {
427
438
  const step = activeHours[1] - activeHours[0];
428
439
  const isRegularStep = activeHours.every((v, i) => v === i * step);
@@ -432,33 +443,26 @@ export function describeCronExpression(expr: string): string {
432
443
  }
433
444
  }
434
445
 
435
- // Specific time patterns: single hour and single minute
436
- if (activeMinutes.length === 1 && activeHours.length === 1 && allMonths) {
446
+ if (fixedTime && allMonths) {
437
447
  const timeStr = formatTime(activeHours[0], activeMinutes[0]);
438
448
 
439
- // Check day-of-week constraints
440
449
  if (allDays && !allDOW) {
441
- // Weekdays: Mon-Fri (1-5)
442
450
  if (activeDOW.length === 5 && activeDOW.every((d) => d >= 1 && d <= 5)) {
443
451
  return `Every weekday at ${timeStr}`;
444
452
  }
445
- // Weekends: Sat, Sun (0, 6)
446
453
  if (activeDOW.length === 2 && activeDOW.includes(0) && activeDOW.includes(6)) {
447
454
  return `Every weekend at ${timeStr}`;
448
455
  }
449
- // Specific days of week
450
456
  const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
451
457
  const names = activeDOW.map((d) => dayNames[d]);
452
458
  return `Every ${names.join(', ')} at ${timeStr}`;
453
459
  }
454
460
 
455
- // Specific day of month
456
461
  if (!allDays && allDOW && activeDays.length === 1) {
457
462
  return `On the ${ordinal(activeDays[0])} of every month at ${timeStr}`;
458
463
  }
459
464
 
460
- // Every day at specific time
461
- if (allDays && allDOW) {
465
+ if (anyDay) {
462
466
  return `Every day at ${timeStr}`;
463
467
  }
464
468
  }
@@ -103,14 +103,14 @@ async function runScheduleOnce(
103
103
  const message = err instanceof Error ? err.message : String(err);
104
104
  log.warn({ err, jobId: job.id, name: job.name, taskId, syntax: job.syntax, expression: job.expression, isRruleSet }, 'Scheduled task execution failed');
105
105
  // Create a fallback conversation for the schedule run record
106
- const fallbackConversation = createConversation(`Schedule: ${job.name}`);
106
+ const fallbackConversation = createConversation({ title: `Schedule: ${job.name}`, source: 'schedule' });
107
107
  const runId = createScheduleRun(job.id, fallbackConversation.id);
108
108
  completeScheduleRun(runId, { status: 'error', error: message });
109
109
  }
110
110
  continue;
111
111
  }
112
112
 
113
- const conversation = createConversation(`Schedule: ${job.name}`);
113
+ const conversation = createConversation({ title: `Schedule: ${job.name}`, source: 'schedule' });
114
114
  const runId = createScheduleRun(job.id, conversation.id);
115
115
  const isRruleSetMsg = job.syntax === 'rrule' && hasSetConstructs(job.expression);
116
116
 
@@ -131,7 +131,7 @@ async function runScheduleOnce(
131
131
  const dueReminders = claimDueReminders(now);
132
132
  for (const reminder of dueReminders) {
133
133
  if (reminder.mode === 'execute') {
134
- const conversation = createConversation(`Reminder: ${reminder.label}`);
134
+ const conversation = createConversation({ title: `Reminder: ${reminder.label}`, source: 'reminder' });
135
135
  setReminderConversationId(reminder.id, conversation.id);
136
136
  try {
137
137
  log.info({ reminderId: reminder.id, label: reminder.label, conversationId: conversation.id }, 'Executing reminder');
@@ -17,8 +17,9 @@ import {
17
17
  createCipheriv,
18
18
  createDecipheriv,
19
19
  } from 'node:crypto';
20
- import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
20
+ import { readFileSync, writeFileSync, chmodSync } from 'node:fs';
21
21
  import { join, dirname } from 'node:path';
22
+ import { pathExists, ensureDir } from '../util/fs.js';
22
23
  import { hostname, userInfo } from 'node:os';
23
24
  import { getRootDir, getPlatformName } from '../util/platform.js';
24
25
  import { getLogger } from '../util/logger.js';
@@ -98,7 +99,7 @@ function deriveKey(salt: Buffer): Buffer {
98
99
  */
99
100
  function readStore(): StoreFile | null {
100
101
  const path = getStorePath();
101
- if (!existsSync(path)) return null;
102
+ if (!pathExists(path)) return null;
102
103
 
103
104
  const raw = readFileSync(path, 'utf-8');
104
105
  const parsed = JSON.parse(raw);
@@ -114,10 +115,7 @@ function readStore(): StoreFile | null {
114
115
 
115
116
  function writeStore(store: StoreFile): void {
116
117
  const path = getStorePath();
117
- const dir = dirname(path);
118
- if (!existsSync(dir)) {
119
- mkdirSync(dir, { recursive: true });
120
- }
118
+ ensureDir(dirname(path));
121
119
  writeFileSync(path, JSON.stringify(store, null, 2), { mode: 0o600 });
122
120
  // Enforce 0600 even if the file already existed with permissive bits
123
121
  chmodSync(path, 0o600);
@@ -125,7 +123,7 @@ function writeStore(store: StoreFile): void {
125
123
 
126
124
  function getOrCreateStore(): StoreFile {
127
125
  const path = getStorePath();
128
- if (existsSync(path)) {
126
+ if (pathExists(path)) {
129
127
  // File exists — must be parseable, otherwise fail to prevent data loss
130
128
  return readStore()!;
131
129
  }
@@ -18,16 +18,25 @@ const log = getLogger('oauth2');
18
18
  // Types
19
19
  // ---------------------------------------------------------------------------
20
20
 
21
+ export type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post';
22
+
21
23
  export interface OAuth2Config {
22
24
  authUrl: string;
23
25
  tokenUrl: string;
24
26
  scopes: string[];
25
27
  clientId: string;
26
- /** Client secret for providers that require it (e.g. Slack). If omitted, PKCE is used. */
28
+ /** Client secret for providers that require it (e.g. Slack). PKCE is always used regardless. */
27
29
  clientSecret?: string;
28
30
  extraParams?: Record<string, string>;
29
31
  /** URL to fetch user identity info after OAuth. If omitted, account info is not fetched. */
30
32
  userinfoUrl?: string;
33
+ /**
34
+ * How the client authenticates at the token endpoint when a clientSecret is present.
35
+ * - `client_secret_post`: Send client_id and client_secret in the POST body (default).
36
+ * - `client_secret_basic`: Send an HTTP Basic Auth header with base64(client_id:client_secret).
37
+ * Defaults to `client_secret_post` for backward compatibility.
38
+ */
39
+ tokenEndpointAuthMethod?: TokenEndpointAuthMethod;
31
40
  }
32
41
 
33
42
  export interface OAuth2TokenResult {
@@ -80,24 +89,32 @@ async function exchangeCodeForTokens(
80
89
  redirectUri: string,
81
90
  codeVerifier: string,
82
91
  ): Promise<OAuth2FlowResult> {
83
- const usePKCE = !config.clientSecret;
92
+ const authMethod = config.tokenEndpointAuthMethod ?? 'client_secret_post';
84
93
 
85
94
  const tokenBody: Record<string, string> = {
86
95
  grant_type: 'authorization_code',
87
96
  code,
88
97
  redirect_uri: redirectUri,
89
- client_id: config.clientId,
98
+ code_verifier: codeVerifier,
90
99
  };
91
- if (usePKCE) {
92
- tokenBody.code_verifier = codeVerifier;
93
- }
94
- if (config.clientSecret) {
95
- tokenBody.client_secret = config.clientSecret;
100
+
101
+ const headers: Record<string, string> = {
102
+ 'Content-Type': 'application/x-www-form-urlencoded',
103
+ };
104
+
105
+ if (config.clientSecret && authMethod === 'client_secret_basic') {
106
+ const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64');
107
+ headers['Authorization'] = `Basic ${credentials}`;
108
+ } else {
109
+ tokenBody.client_id = config.clientId;
110
+ if (config.clientSecret) {
111
+ tokenBody.client_secret = config.clientSecret;
112
+ }
96
113
  }
97
114
 
98
115
  const tokenResp = await fetch(config.tokenUrl, {
99
116
  method: 'POST',
100
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
117
+ headers,
101
118
  body: new URLSearchParams(tokenBody),
102
119
  });
103
120
 
@@ -160,7 +177,6 @@ async function runGatewayFlow(
160
177
  registerPendingCallback(state, resolve, reject);
161
178
  });
162
179
 
163
- const usePKCE = !config.clientSecret;
164
180
  const authParams = new URLSearchParams({
165
181
  ...config.extraParams,
166
182
  client_id: config.clientId,
@@ -168,7 +184,8 @@ async function runGatewayFlow(
168
184
  response_type: 'code',
169
185
  scope: config.scopes.join(' '),
170
186
  state,
171
- ...(usePKCE ? { code_challenge: codeChallenge, code_challenge_method: 'S256' } : {}),
187
+ code_challenge: codeChallenge,
188
+ code_challenge_method: 'S256',
172
189
  });
173
190
 
174
191
  const authUrl = `${config.authUrl}?${authParams}`;
@@ -230,19 +247,32 @@ export async function refreshOAuth2Token(
230
247
  clientId: string,
231
248
  refreshToken: string,
232
249
  clientSecret?: string,
250
+ tokenEndpointAuthMethod?: TokenEndpointAuthMethod,
233
251
  ): Promise<OAuth2TokenResult> {
252
+ const authMethod = tokenEndpointAuthMethod ?? 'client_secret_post';
253
+
234
254
  const body: Record<string, string> = {
235
255
  grant_type: 'refresh_token',
236
256
  refresh_token: refreshToken,
237
- client_id: clientId,
238
257
  };
239
- if (clientSecret) {
240
- body.client_secret = clientSecret;
258
+
259
+ const headers: Record<string, string> = {
260
+ 'Content-Type': 'application/x-www-form-urlencoded',
261
+ };
262
+
263
+ if (clientSecret && authMethod === 'client_secret_basic') {
264
+ const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
265
+ headers['Authorization'] = `Basic ${credentials}`;
266
+ } else {
267
+ body.client_id = clientId;
268
+ if (clientSecret) {
269
+ body.client_secret = clientSecret;
270
+ }
241
271
  }
242
272
 
243
273
  const resp = await fetch(tokenUrl, {
244
274
  method: 'POST',
245
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
275
+ headers,
246
276
  body: new URLSearchParams(body),
247
277
  });
248
278
 
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Parental control settings and PIN management.
3
+ *
4
+ * Non-secret settings (enabled state, content restrictions, blocked tool
5
+ * categories) are persisted to `~/.vellum/parental-control.json`.
6
+ *
7
+ * The PIN hash and salt are stored in the encrypted key store under the
8
+ * account `parental:pin` as the hex string `"<salt>:<hash>"`.
9
+ *
10
+ * PIN hashing uses SHA-256 with a random 16-byte salt to prevent offline
11
+ * dictionary attacks. Comparison uses timingSafeEqual to avoid timing leaks.
12
+ */
13
+
14
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
15
+ import { readFileSync, writeFileSync } from 'node:fs';
16
+ import { join, dirname } from 'node:path';
17
+ import { pathExists, ensureDir } from '../util/fs.js';
18
+ import { getRootDir } from '../util/platform.js';
19
+ import { getKey, setKey, deleteKey } from './encrypted-store.js';
20
+ import { getLogger } from '../util/logger.js';
21
+ import type { ParentalContentTopic, ParentalToolCategory } from '../daemon/ipc-contract/parental-control.js';
22
+
23
+ const log = getLogger('parental-control');
24
+
25
+ const PIN_ACCOUNT = 'parental:pin';
26
+
27
+ export type { ParentalContentTopic, ParentalToolCategory };
28
+
29
+ export interface ParentalControlSettings {
30
+ enabled: boolean;
31
+ contentRestrictions: ParentalContentTopic[];
32
+ blockedToolCategories: ParentalToolCategory[];
33
+ }
34
+
35
+ const DEFAULT_SETTINGS: ParentalControlSettings = {
36
+ enabled: false,
37
+ contentRestrictions: [],
38
+ blockedToolCategories: [],
39
+ };
40
+
41
+ function getSettingsPath(): string {
42
+ return join(getRootDir(), 'parental-control.json');
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Settings I/O
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export function getParentalControlSettings(): ParentalControlSettings {
50
+ try {
51
+ const file = getSettingsPath();
52
+ if (!pathExists(file)) return { ...DEFAULT_SETTINGS };
53
+ const raw = readFileSync(file, 'utf-8');
54
+ const parsed = JSON.parse(raw) as Partial<ParentalControlSettings>;
55
+ return {
56
+ enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : false,
57
+ contentRestrictions: Array.isArray(parsed.contentRestrictions) ? parsed.contentRestrictions : [],
58
+ blockedToolCategories: Array.isArray(parsed.blockedToolCategories) ? parsed.blockedToolCategories : [],
59
+ };
60
+ } catch {
61
+ return { ...DEFAULT_SETTINGS };
62
+ }
63
+ }
64
+
65
+ function saveSettings(settings: ParentalControlSettings): void {
66
+ const file = getSettingsPath();
67
+ ensureDir(dirname(file));
68
+ writeFileSync(file, JSON.stringify(settings, null, 2), { encoding: 'utf-8' });
69
+ }
70
+
71
+ export function updateParentalControlSettings(
72
+ patch: Partial<ParentalControlSettings>,
73
+ ): ParentalControlSettings {
74
+ const current = getParentalControlSettings();
75
+ const next: ParentalControlSettings = {
76
+ enabled: patch.enabled !== undefined ? patch.enabled : current.enabled,
77
+ contentRestrictions: patch.contentRestrictions !== undefined
78
+ ? patch.contentRestrictions
79
+ : current.contentRestrictions,
80
+ blockedToolCategories: patch.blockedToolCategories !== undefined
81
+ ? patch.blockedToolCategories
82
+ : current.blockedToolCategories,
83
+ };
84
+ saveSettings(next);
85
+ return next;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // PIN management
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /** Returns true if a parental control PIN has been configured. */
93
+ export function hasPIN(): boolean {
94
+ return getKey(PIN_ACCOUNT) !== undefined;
95
+ }
96
+
97
+ function hashPIN(pin: string, salt: Buffer): Buffer {
98
+ return createHash('sha256').update(salt).update(pin).digest();
99
+ }
100
+
101
+ /**
102
+ * Set a new PIN. Rejects if `pin` is not exactly 6 ASCII digits.
103
+ * Throws if the store write fails.
104
+ */
105
+ export function setPIN(pin: string): void {
106
+ if (!/^\d{6}$/.test(pin)) {
107
+ throw new Error('PIN must be exactly 6 digits');
108
+ }
109
+ const salt = randomBytes(16);
110
+ const hash = hashPIN(pin, salt);
111
+ const stored = `${salt.toString('hex')}:${hash.toString('hex')}`;
112
+ if (!setKey(PIN_ACCOUNT, stored)) {
113
+ throw new Error('Failed to persist PIN — encrypted store write error');
114
+ }
115
+ log.info('Parental control PIN set');
116
+ }
117
+
118
+ /**
119
+ * Verify a PIN attempt. Returns true on match, false on mismatch or if no
120
+ * PIN has been configured. Uses constant-time comparison to prevent timing
121
+ * attacks.
122
+ */
123
+ export function verifyPIN(pin: string): boolean {
124
+ if (!/^\d{6}$/.test(pin)) return false;
125
+ const stored = getKey(PIN_ACCOUNT);
126
+ if (!stored) return false;
127
+
128
+ const colonIdx = stored.indexOf(':');
129
+ if (colonIdx === -1) return false;
130
+
131
+ try {
132
+ const salt = Buffer.from(stored.slice(0, colonIdx), 'hex');
133
+ const expectedHash = Buffer.from(stored.slice(colonIdx + 1), 'hex');
134
+ const actualHash = hashPIN(pin, salt);
135
+ if (actualHash.length !== expectedHash.length) return false;
136
+ return timingSafeEqual(actualHash, expectedHash);
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Remove the PIN. The caller is responsible for requiring PIN verification
144
+ * before calling this.
145
+ */
146
+ export function clearPIN(): void {
147
+ deleteKey(PIN_ACCOUNT);
148
+ log.info('Parental control PIN cleared');
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Tool category → tool name mapping
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /**
156
+ * Tool name prefixes that belong to each blocked category.
157
+ * A tool is considered blocked if its name starts with any of the listed
158
+ * prefixes (case-sensitive).
159
+ */
160
+ export const TOOL_CATEGORY_PREFIXES: Record<ParentalToolCategory, string[]> = {
161
+ computer_use: ['cu_', 'computer_use', 'screenshot', 'accessibility_'],
162
+ network: ['web_fetch', 'web_search', 'browser_'],
163
+ shell: ['bash', 'terminal', 'host_shell'],
164
+ file_write: ['file_write', 'file_edit', 'multi_edit', 'file_delete', 'git'],
165
+ };
166
+
167
+ /**
168
+ * Returns true if the given tool name falls within any of the currently
169
+ * blocked tool categories.
170
+ */
171
+ export function isToolBlocked(toolName: string): boolean {
172
+ const { enabled, blockedToolCategories } = getParentalControlSettings();
173
+ if (!enabled || blockedToolCategories.length === 0) return false;
174
+
175
+ for (const category of blockedToolCategories) {
176
+ const prefixes = TOOL_CATEGORY_PREFIXES[category];
177
+ // Guard against unknown categories that may appear after deserialization of
178
+ // settings written by a newer client version — skip rather than throw.
179
+ if (!prefixes) continue;
180
+ if (prefixes.some((p) => toolName.startsWith(p))) return true;
181
+ }
182
+ return false;
183
+ }
@@ -13,9 +13,10 @@
13
13
  * }
14
14
  */
15
15
 
16
- import { readFileSync, existsSync } from 'node:fs';
16
+ import { readFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
18
  import { getRootDir } from '../util/platform.js';
19
+ import { pathExists } from '../util/fs.js';
19
20
  import { getLogger } from '../util/logger.js';
20
21
 
21
22
  const log = getLogger('secret-allowlist');
@@ -44,7 +45,7 @@ export function loadAllowlist(): void {
44
45
  if (loaded || fileChecked) return;
45
46
 
46
47
  const filePath = join(getRootDir(), 'protected', 'secret-allowlist.json');
47
- if (!existsSync(filePath)) {
48
+ if (!pathExists(filePath)) {
48
49
  fileChecked = true;
49
50
  return;
50
51
  }
@@ -143,7 +144,7 @@ export function validateAllowlist(config: AllowlistConfig): AllowlistValidationE
143
144
  */
144
145
  export function validateAllowlistFile(): AllowlistValidationError[] | null {
145
146
  const filePath = join(getRootDir(), 'protected', 'secret-allowlist.json');
146
- if (!existsSync(filePath)) return null;
147
+ if (!pathExists(filePath)) return null;
147
148
 
148
149
  const raw = readFileSync(filePath, 'utf-8');
149
150
  const config: AllowlistConfig = JSON.parse(raw);
@@ -393,7 +393,7 @@ function scanEntropy(
393
393
  // Scan hex tokens
394
394
  HEX_TOKEN_RE.lastIndex = 0;
395
395
  let m: RegExpExecArray | null;
396
- while ((m = HEX_TOKEN_RE.exec(text)) !== null) {
396
+ while ((m = HEX_TOKEN_RE.exec(text)) != null) {
397
397
  const value = m[1];
398
398
  if (value.length < config.minLength) continue;
399
399
  const startIndex = m.index;
@@ -424,7 +424,7 @@ function scanEntropy(
424
424
 
425
425
  // Scan base64 tokens
426
426
  BASE64_TOKEN_RE.lastIndex = 0;
427
- while ((m = BASE64_TOKEN_RE.exec(text)) !== null) {
427
+ while ((m = BASE64_TOKEN_RE.exec(text)) != null) {
428
428
  const value = m[1];
429
429
  if (value.length < config.minLength) continue;
430
430
  // Must look like base64 (not pure alphanumeric) or pure hex
@@ -603,7 +603,7 @@ function scanEncoded(
603
603
  for (const pattern of PATTERNS) {
604
604
  pattern.regex.lastIndex = 0;
605
605
  let pm: RegExpExecArray | null;
606
- while ((pm = pattern.regex.exec(decoded)) !== null) {
606
+ while ((pm = pattern.regex.exec(decoded)) != null) {
607
607
  const value = pm[1] ?? pm[0];
608
608
  if (isPlaceholder(value)) continue;
609
609
  if (isAllowlisted(value)) continue;
@@ -649,7 +649,7 @@ function scanEncoded(
649
649
  if (quickCheck && !quickCheck(text)) continue;
650
650
  regex.lastIndex = 0;
651
651
  let m: RegExpExecArray | null;
652
- while ((m = regex.exec(text)) !== null) {
652
+ while ((m = regex.exec(text)) != null) {
653
653
  const encoded = m[1] ?? m[0];
654
654
  if (encoded.length > 1000) continue;
655
655
  const startIndex = m.index + (m[0].indexOf(encoded));
@@ -688,7 +688,7 @@ export function scanText(text: string, entropyConfig?: Partial<EntropyConfig>):
688
688
  // Reset lastIndex for global regexes
689
689
  pattern.regex.lastIndex = 0;
690
690
  let m: RegExpExecArray | null;
691
- while ((m = pattern.regex.exec(text)) !== null) {
691
+ while ((m = pattern.regex.exec(text)) != null) {
692
692
  // Use first capturing group if present, otherwise full match
693
693
  const value = m[1] ?? m[0];
694
694
  const startIndex = m.index + (m[0].indexOf(value));
@@ -123,7 +123,7 @@ export function deleteSecureKey(account: string): boolean {
123
123
  // backend — saveConfig routinely deletes keys for unset providers.
124
124
  // getKey now returns null for "not found" and throws on runtime errors.
125
125
  try {
126
- if (keychain.getKey(account) === null) {
126
+ if (keychain.getKey(account) == null) {
127
127
  return false;
128
128
  }
129
129
  } catch {
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { getSecureKey, setSecureKey } from './secure-keys.js';
10
10
  import { getCredentialMetadata, upsertCredentialMetadata } from '../tools/credentials/metadata-store.js';
11
- import { refreshOAuth2Token } from './oauth2.js';
11
+ import { refreshOAuth2Token, type TokenEndpointAuthMethod } from './oauth2.js';
12
12
  import { getLogger } from '../util/logger.js';
13
13
 
14
14
  const log = getLogger('token-manager');
@@ -66,11 +66,12 @@ async function doRefresh(service: string): Promise<string> {
66
66
  }
67
67
 
68
68
  const clientSecret = meta?.oauth2ClientSecret as string | undefined;
69
+ const authMethod = meta?.oauth2TokenEndpointAuthMethod as TokenEndpointAuthMethod | undefined;
69
70
  const resolvedTokenUrl = tokenUrl;
70
71
 
71
72
  log.info({ service }, 'Refreshing OAuth2 access token');
72
73
 
73
- const result = await refreshOAuth2Token(resolvedTokenUrl, clientId, refreshToken, clientSecret);
74
+ const result = await refreshOAuth2Token(resolvedTokenUrl, clientId, refreshToken, clientSecret, authMethod);
74
75
 
75
76
  if (!setSecureKey(`credential:${service}:access_token`, result.accessToken)) {
76
77
  throw new Error(`Failed to store refreshed access token for "${service}"`);
@@ -2,6 +2,8 @@
2
2
  * Thin wrapper around the Vercel REST API for deploying static HTML pages.
3
3
  */
4
4
 
5
+ import { ProviderError } from '../util/errors.js';
6
+
5
7
  export async function deployHtmlToVercel(opts: {
6
8
  html: string;
7
9
  name: string;
@@ -37,7 +39,7 @@ export async function deployHtmlToVercel(opts: {
37
39
 
38
40
  if (!response.ok) {
39
41
  const text = await response.text();
40
- throw new Error(`Vercel deploy failed (${response.status}): ${text}`);
42
+ throw new ProviderError(`Vercel deploy failed (${response.status}): ${text}`, 'vercel', response.status);
41
43
  }
42
44
 
43
45
  const data = (await response.json()) as { url: string; id: string };
@@ -66,8 +68,10 @@ export async function deleteVercelDeployment(
66
68
 
67
69
  if (!response.ok) {
68
70
  const text = await response.text();
69
- throw new Error(
71
+ throw new ProviderError(
70
72
  `Vercel delete deployment failed (${response.status}): ${text}`,
73
+ 'vercel',
74
+ response.status,
71
75
  );
72
76
  }
73
77
  }