@vellumai/assistant 0.4.46 → 0.4.49

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 (382) hide show
  1. package/ARCHITECTURE.md +7 -7
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/architecture/security.md +5 -5
  6. package/docs/runbook-trusted-contacts.md +3 -8
  7. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  8. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  9. package/package.json +1 -1
  10. package/src/__tests__/actor-token-service.test.ts +0 -1
  11. package/src/__tests__/anthropic-provider.test.ts +156 -0
  12. package/src/__tests__/approval-cascade.test.ts +810 -0
  13. package/src/__tests__/approval-primitive.test.ts +0 -1
  14. package/src/__tests__/approval-routes-http.test.ts +2 -0
  15. package/src/__tests__/assistant-attachments.test.ts +12 -34
  16. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  17. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  18. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  19. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  20. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  21. package/src/__tests__/channel-guardian.test.ts +0 -2
  22. package/src/__tests__/channel-readiness-routes.test.ts +35 -25
  23. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  24. package/src/__tests__/checker.test.ts +9 -29
  25. package/src/__tests__/cli.test.ts +23 -0
  26. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  27. package/src/__tests__/computer-use-tools.test.ts +2 -19
  28. package/src/__tests__/config-watcher.test.ts +0 -1
  29. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  30. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  31. package/src/__tests__/context-token-estimator.test.ts +196 -13
  32. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  33. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  34. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  36. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  37. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  38. package/src/__tests__/credential-broker.test.ts +2 -1
  39. package/src/__tests__/credential-metadata-store.test.ts +239 -26
  40. package/src/__tests__/credential-resolve.test.ts +5 -4
  41. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  42. package/src/__tests__/credential-security-invariants.test.ts +111 -7
  43. package/src/__tests__/credential-vault-unit.test.ts +287 -54
  44. package/src/__tests__/credential-vault.test.ts +406 -12
  45. package/src/__tests__/credentials-cli.test.ts +82 -6
  46. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  47. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  48. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  49. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  50. package/src/__tests__/gemini-image-service.test.ts +75 -45
  51. package/src/__tests__/gemini-provider.test.ts +9 -6
  52. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  53. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  54. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  55. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  56. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  57. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  58. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  59. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  60. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  61. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  62. package/src/__tests__/heartbeat-service.test.ts +0 -1
  63. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  64. package/src/__tests__/host-shell-tool.test.ts +27 -15
  65. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  66. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  67. package/src/__tests__/integration-status.test.ts +38 -25
  68. package/src/__tests__/intent-routing.test.ts +0 -1
  69. package/src/__tests__/invite-routes-http.test.ts +10 -9
  70. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  71. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  72. package/src/__tests__/media-generate-image.test.ts +63 -2
  73. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  74. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  75. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  76. package/src/__tests__/oauth-cli.test.ts +373 -14
  77. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  78. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  79. package/src/__tests__/oauth-store.test.ts +756 -0
  80. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  81. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  82. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  83. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  84. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  85. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  86. package/src/__tests__/recording-handler.test.ts +3 -4
  87. package/src/__tests__/registry.test.ts +2 -2
  88. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  89. package/src/__tests__/schedule-store.test.ts +0 -1
  90. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  91. package/src/__tests__/schema-transforms.test.ts +226 -0
  92. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  93. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  94. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  95. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  96. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  97. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  98. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  99. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  100. package/src/__tests__/sequence-store.test.ts +0 -1
  101. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  102. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  103. package/src/__tests__/skill-include-graph.test.ts +66 -0
  104. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  105. package/src/__tests__/skill-load-tool.test.ts +149 -1
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  107. package/src/__tests__/skills-uninstall.test.ts +3 -3
  108. package/src/__tests__/skills.test.ts +3 -12
  109. package/src/__tests__/slack-channel-config.test.ts +76 -11
  110. package/src/__tests__/slack-share-routes.test.ts +17 -14
  111. package/src/__tests__/system-prompt.test.ts +0 -1
  112. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  113. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  114. package/src/__tests__/terminal-tools.test.ts +4 -3
  115. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  116. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  117. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  118. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  119. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  120. package/src/__tests__/tool-executor.test.ts +0 -1
  121. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  122. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  123. package/src/__tests__/trust-store.test.ts +1 -22
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  126. package/src/__tests__/twilio-config.test.ts +2 -1
  127. package/src/__tests__/twilio-provider.test.ts +4 -2
  128. package/src/__tests__/twilio-routes.test.ts +5 -20
  129. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  130. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  131. package/src/agent/ax-tree-compaction.test.ts +235 -0
  132. package/src/agent/loop.ts +76 -130
  133. package/src/calls/call-domain.ts +8 -10
  134. package/src/calls/relay-server.ts +9 -13
  135. package/src/calls/twilio-config.ts +4 -8
  136. package/src/calls/twilio-provider.ts +2 -1
  137. package/src/calls/twilio-rest.ts +2 -1
  138. package/src/calls/twilio-routes.ts +1 -2
  139. package/src/calls/voice-ingress-preflight.ts +1 -1
  140. package/src/cli/commands/browser-relay.ts +46 -15
  141. package/src/cli/commands/completions.ts +0 -3
  142. package/src/cli/commands/credentials.ts +110 -23
  143. package/src/cli/commands/oauth/apps.ts +255 -0
  144. package/src/cli/commands/oauth/connections.ts +299 -0
  145. package/src/cli/commands/oauth/index.ts +52 -0
  146. package/src/cli/commands/oauth/providers.ts +242 -0
  147. package/src/cli/commands/skills.ts +4 -338
  148. package/src/cli/program.ts +1 -5
  149. package/src/cli/reference.ts +1 -3
  150. package/src/cli.ts +3 -2
  151. package/src/config/assistant-feature-flags.ts +0 -3
  152. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  153. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  154. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  155. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  156. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  157. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  158. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  159. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  160. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  161. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  162. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  163. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  164. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  165. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  166. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  167. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  168. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  169. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  170. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  171. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  172. package/src/config/bundled-skills/google-calendar/calendar-client.ts +90 -44
  173. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  174. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  175. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  176. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  177. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  178. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  179. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  180. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  181. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  182. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  183. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  184. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  185. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  186. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  187. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  188. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  189. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  190. package/src/config/bundled-skills/messaging/tools/shared.ts +12 -15
  191. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  192. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  193. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  194. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  195. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  196. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  197. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  198. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  199. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  200. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  201. package/src/config/env-registry.ts +14 -83
  202. package/src/config/env.ts +11 -50
  203. package/src/config/feature-flag-registry.json +16 -16
  204. package/src/config/schema.ts +3 -1
  205. package/src/config/skills.ts +21 -2
  206. package/src/context/image-dimensions.ts +229 -0
  207. package/src/context/token-estimator.ts +75 -12
  208. package/src/context/window-manager.ts +49 -10
  209. package/src/daemon/assistant-attachments.ts +1 -13
  210. package/src/daemon/guardian-action-generators.ts +4 -5
  211. package/src/daemon/handlers/config-ingress.ts +8 -33
  212. package/src/daemon/handlers/config-slack-channel.ts +76 -56
  213. package/src/daemon/handlers/config-telegram.ts +53 -24
  214. package/src/daemon/handlers/sessions.ts +10 -24
  215. package/src/daemon/handlers/shared.ts +0 -130
  216. package/src/daemon/host-cu-proxy.ts +401 -0
  217. package/src/daemon/lifecycle.ts +39 -63
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/computer-use.ts +2 -119
  220. package/src/daemon/message-types/host-cu.ts +19 -0
  221. package/src/daemon/message-types/integrations.ts +1 -0
  222. package/src/daemon/message-types/messages.ts +3 -0
  223. package/src/daemon/server.ts +14 -21
  224. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  225. package/src/daemon/session-attachments.ts +1 -2
  226. package/src/daemon/session-messaging.ts +3 -1
  227. package/src/daemon/session-slash.ts +1 -1
  228. package/src/daemon/session-surfaces.ts +40 -28
  229. package/src/daemon/session-tool-setup.ts +20 -11
  230. package/src/daemon/session.ts +139 -16
  231. package/src/daemon/tool-side-effects.ts +2 -8
  232. package/src/daemon/watch-handler.ts +2 -2
  233. package/src/email/providers/index.ts +2 -1
  234. package/src/events/tool-metrics-listener.ts +2 -2
  235. package/src/hooks/manager.ts +1 -4
  236. package/src/inbound/public-ingress-urls.ts +7 -7
  237. package/src/instrument.ts +15 -1
  238. package/src/logfire.ts +16 -5
  239. package/src/media/app-icon-generator.ts +30 -4
  240. package/src/media/avatar-router.ts +26 -3
  241. package/src/media/gemini-image-service.ts +28 -2
  242. package/src/memory/conversation-key-store.ts +21 -0
  243. package/src/memory/db-init.ts +4 -0
  244. package/src/memory/guardian-action-store.ts +1 -1
  245. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  246. package/src/memory/migrations/index.ts +1 -0
  247. package/src/memory/schema/guardian.ts +1 -1
  248. package/src/memory/schema/index.ts +1 -0
  249. package/src/memory/schema/oauth.ts +65 -0
  250. package/src/messaging/provider.ts +19 -13
  251. package/src/messaging/providers/gmail/adapter.ts +40 -23
  252. package/src/messaging/providers/gmail/client.ts +283 -122
  253. package/src/messaging/providers/gmail/people-client.ts +32 -24
  254. package/src/messaging/providers/slack/adapter.ts +29 -19
  255. package/src/messaging/providers/slack/client.ts +265 -78
  256. package/src/messaging/providers/telegram-bot/adapter.ts +19 -18
  257. package/src/messaging/providers/whatsapp/adapter.ts +17 -11
  258. package/src/messaging/registry.ts +2 -31
  259. package/src/notifications/copy-composer.ts +0 -5
  260. package/src/notifications/signal.ts +4 -5
  261. package/src/oauth/byo-connection.test.ts +537 -0
  262. package/src/oauth/byo-connection.ts +128 -0
  263. package/src/oauth/connect-orchestrator.ts +139 -56
  264. package/src/oauth/connect-types.ts +17 -23
  265. package/src/oauth/connection-resolver.ts +58 -0
  266. package/src/oauth/connection.ts +38 -0
  267. package/src/oauth/manual-token-connection.ts +104 -0
  268. package/src/oauth/oauth-store.ts +496 -0
  269. package/src/oauth/platform-connection.test.ts +192 -0
  270. package/src/oauth/platform-connection.ts +111 -0
  271. package/src/oauth/provider-behaviors.ts +124 -0
  272. package/src/oauth/scope-policy.ts +9 -2
  273. package/src/oauth/seed-providers.ts +161 -0
  274. package/src/oauth/token-persistence.ts +74 -78
  275. package/src/permissions/checker.ts +8 -4
  276. package/src/permissions/defaults.ts +0 -1
  277. package/src/permissions/prompter.ts +10 -1
  278. package/src/permissions/trust-store.ts +13 -0
  279. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  280. package/src/prompts/system-prompt.ts +70 -45
  281. package/src/providers/anthropic/client.ts +133 -24
  282. package/src/providers/gemini/client.ts +15 -6
  283. package/src/providers/managed-proxy/constants.ts +2 -2
  284. package/src/providers/managed-proxy/context.ts +5 -1
  285. package/src/providers/ratelimit.ts +17 -0
  286. package/src/providers/registry.ts +2 -2
  287. package/src/providers/retry.ts +1 -27
  288. package/src/runtime/AGENTS.md +17 -0
  289. package/src/runtime/auth/route-policy.ts +0 -3
  290. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  291. package/src/runtime/channel-readiness-service.ts +168 -195
  292. package/src/runtime/channel-readiness-types.ts +4 -0
  293. package/src/runtime/channel-reply-delivery.ts +0 -40
  294. package/src/runtime/gateway-client.ts +0 -7
  295. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  296. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  297. package/src/runtime/guardian-action-message-composer.ts +3 -23
  298. package/src/runtime/http-server.ts +17 -10
  299. package/src/runtime/http-types.ts +2 -3
  300. package/src/runtime/middleware/rate-limiter.ts +74 -20
  301. package/src/runtime/middleware/twilio-validation.ts +1 -11
  302. package/src/runtime/pending-interactions.ts +14 -12
  303. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  304. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  305. package/src/runtime/routes/conversation-routes.ts +73 -19
  306. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  307. package/src/runtime/routes/events-routes.ts +21 -11
  308. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  309. package/src/runtime/routes/host-cu-routes.ts +97 -0
  310. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  311. package/src/runtime/routes/integrations/slack/share.ts +6 -6
  312. package/src/runtime/routes/integrations/twilio.ts +6 -5
  313. package/src/runtime/routes/log-export-routes.ts +126 -8
  314. package/src/runtime/routes/secret-routes.ts +3 -2
  315. package/src/runtime/routes/settings-routes.ts +113 -48
  316. package/src/runtime/routes/surface-action-routes.ts +1 -1
  317. package/src/runtime/routes/watch-routes.ts +128 -0
  318. package/src/schedule/integration-status.ts +10 -8
  319. package/src/security/credential-key.ts +14 -0
  320. package/src/security/keychain-broker-client.ts +5 -6
  321. package/src/security/oauth2.ts +1 -1
  322. package/src/security/token-manager.ts +145 -43
  323. package/src/skills/catalog-install.ts +358 -0
  324. package/src/skills/include-graph.ts +32 -0
  325. package/src/telegram/bot-username.ts +2 -3
  326. package/src/tools/apps/definitions.ts +0 -5
  327. package/src/tools/assets/materialize.ts +0 -5
  328. package/src/tools/assets/search.ts +0 -5
  329. package/src/tools/browser/headless-browser.ts +1 -67
  330. package/src/tools/browser/network-recorder.ts +1 -1
  331. package/src/tools/browser/network-recording-types.ts +1 -1
  332. package/src/tools/claude-code/claude-code.ts +0 -5
  333. package/src/tools/computer-use/definitions.ts +46 -11
  334. package/src/tools/computer-use/registry.ts +4 -5
  335. package/src/tools/credentials/broker.ts +5 -4
  336. package/src/tools/credentials/metadata-store.ts +22 -74
  337. package/src/tools/credentials/resolve.ts +2 -1
  338. package/src/tools/credentials/vault.ts +139 -151
  339. package/src/tools/filesystem/edit.ts +1 -6
  340. package/src/tools/filesystem/read.ts +0 -5
  341. package/src/tools/filesystem/write.ts +1 -6
  342. package/src/tools/host-filesystem/edit.ts +1 -6
  343. package/src/tools/host-filesystem/read.ts +1 -6
  344. package/src/tools/host-filesystem/write.ts +1 -6
  345. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  346. package/src/tools/memory/definitions.ts +0 -5
  347. package/src/tools/network/web-fetch.ts +0 -5
  348. package/src/tools/network/web-search.ts +0 -5
  349. package/src/tools/registry.ts +2 -7
  350. package/src/tools/schema-transforms.ts +99 -0
  351. package/src/tools/skills/load.ts +62 -8
  352. package/src/tools/swarm/delegate.ts +0 -5
  353. package/src/tools/system/avatar-generator.ts +0 -5
  354. package/src/tools/ui-surface/definitions.ts +0 -15
  355. package/src/tools/watch/screen-watch.ts +0 -5
  356. package/src/tools/watch/watch-state.ts +0 -12
  357. package/src/util/logger.ts +7 -41
  358. package/src/util/platform.ts +9 -28
  359. package/src/version.ts +10 -0
  360. package/src/watcher/providers/github.ts +51 -52
  361. package/src/watcher/providers/gmail.ts +88 -80
  362. package/src/watcher/providers/google-calendar.ts +94 -86
  363. package/src/watcher/providers/linear.ts +87 -93
  364. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  365. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  366. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  367. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  368. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  369. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  370. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  371. package/src/cli/commands/dev.ts +0 -129
  372. package/src/cli/commands/map.ts +0 -391
  373. package/src/cli/commands/oauth.ts +0 -77
  374. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  375. package/src/daemon/computer-use-session.ts +0 -1020
  376. package/src/daemon/ride-shotgun-handler.ts +0 -567
  377. package/src/oauth/provider-profiles.ts +0 -192
  378. package/src/prompts/computer-use-prompt.ts +0 -98
  379. package/src/runtime/routes/computer-use-routes.ts +0 -641
  380. package/src/runtime/telegram-streaming-delivery.test.ts +0 -597
  381. package/src/runtime/telegram-streaming-delivery.ts +0 -383
  382. package/src/tools/computer-use/request-computer-control.ts +0 -61
@@ -45,17 +45,95 @@ mock.module("../tools/registry.js", () => ({
45
45
  registerTool: () => {},
46
46
  }));
47
47
 
48
+ // ---------------------------------------------------------------------------
49
+ // Mock OAuth2 token refresh for token-manager deduplication tests
50
+ // ---------------------------------------------------------------------------
51
+
52
+ let mockRefreshOAuth2Token: ReturnType<
53
+ typeof mock<() => Promise<{ accessToken: string; expiresIn: number }>>
54
+ >;
55
+
56
+ mock.module("../security/oauth2.js", () => {
57
+ mockRefreshOAuth2Token = mock(() =>
58
+ Promise.resolve({
59
+ accessToken: "refreshed-access-token",
60
+ expiresIn: 3600,
61
+ }),
62
+ );
63
+ return {
64
+ refreshOAuth2Token: mockRefreshOAuth2Token,
65
+ };
66
+ });
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Mock oauth-store — token-manager reads refresh config from SQLite
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /** Mutable per-test map of provider connections for getConnectionByProvider */
73
+ const mockConnections = new Map<
74
+ string,
75
+ {
76
+ id: string;
77
+ providerKey: string;
78
+ oauthAppId: string;
79
+ expiresAt: number | null;
80
+ }
81
+ >();
82
+ const mockApps = new Map<
83
+ string,
84
+ { id: string; providerKey: string; clientId: string }
85
+ >();
86
+ const mockProviders = new Map<
87
+ string,
88
+ {
89
+ key: string;
90
+ tokenUrl: string;
91
+ tokenEndpointAuthMethod?: string;
92
+ }
93
+ >();
94
+
95
+ let mockDisconnectOAuthProvider: ReturnType<
96
+ typeof mock<
97
+ (providerKey: string) => Promise<"disconnected" | "not-found" | "error">
98
+ >
99
+ >;
100
+
101
+ mock.module("../oauth/oauth-store.js", () => {
102
+ mockDisconnectOAuthProvider = mock((providerKey: string) =>
103
+ Promise.resolve(
104
+ mockConnections.has(providerKey)
105
+ ? ("disconnected" as const)
106
+ : ("not-found" as const),
107
+ ),
108
+ );
109
+ return {
110
+ disconnectOAuthProvider: mockDisconnectOAuthProvider,
111
+ getConnectionByProvider: (service: string) => mockConnections.get(service),
112
+ getApp: (id: string) => mockApps.get(id),
113
+ getProvider: (key: string) => mockProviders.get(key),
114
+ updateConnection: () => {},
115
+ getMostRecentAppByProvider: () => undefined,
116
+ listConnections: () => [],
117
+ };
118
+ });
119
+
48
120
  // ---------------------------------------------------------------------------
49
121
  // Import the module under test
50
122
  // ---------------------------------------------------------------------------
51
123
 
52
124
  // getCredentialValue is no longer exported (sealed in PR 17) — use getSecureKey directly
53
125
 
126
+ import { credentialKey } from "../security/credential-key.js";
54
127
  import {
55
128
  deleteSecureKey,
56
129
  getSecureKey,
57
130
  setSecureKey,
58
131
  } from "../security/secure-keys.js";
132
+ import {
133
+ _resetInflightRefreshes,
134
+ _resetRefreshBreakers,
135
+ withValidToken,
136
+ } from "../security/token-manager.js";
59
137
  import {
60
138
  _setMetadataPath,
61
139
  getCredentialMetadata,
@@ -120,7 +198,7 @@ async function executeVault(
120
198
  };
121
199
  }
122
200
 
123
- const key = `credential:${service}:${field}`;
201
+ const key = credentialKey(service, field);
124
202
  const ok = setSecureKey(key, value);
125
203
  if (!ok) {
126
204
  return { content: "Error: failed to store credential", isError: true };
@@ -151,7 +229,7 @@ async function executeVault(
151
229
  };
152
230
  }
153
231
 
154
- const key = `credential:${service}:${field}`;
232
+ const key = credentialKey(service, field);
155
233
  const result = deleteSecureKey(key);
156
234
  if (result !== "deleted") {
157
235
  return {
@@ -187,12 +265,15 @@ describe("credential_store tool", () => {
187
265
  }
188
266
  _setStorePath(STORE_PATH);
189
267
  _setMetadataPath(join(TEST_DIR, "metadata.json"));
268
+ mockDisconnectOAuthProvider.mockClear();
269
+ mockConnections.clear();
190
270
  });
191
271
 
192
272
  afterEach(() => {
193
273
  _setMetadataPath(null);
194
274
  _setStorePath(null);
195
275
  _resetBackend();
276
+ mockConnections.clear();
196
277
  });
197
278
 
198
279
  afterAll(() => {
@@ -553,7 +634,7 @@ describe("credential_store tool", () => {
553
634
 
554
635
  // Delete the secret directly without going through the tool (simulates
555
636
  // a divergence where metadata write failed after secret deletion)
556
- deleteSecureKey("credential:svc-a:key");
637
+ deleteSecureKey(credentialKey("svc-a", "key"));
557
638
 
558
639
  const result = await credentialStoreTool.execute(
559
640
  { action: "list" },
@@ -596,7 +677,7 @@ describe("credential_store tool", () => {
596
677
  // -----------------------------------------------------------------------
597
678
  describe("delete action", () => {
598
679
  test("deletes a stored credential", async () => {
599
- setSecureKey("credential:gmail:password", "secret");
680
+ setSecureKey(credentialKey("gmail", "password"), "secret");
600
681
 
601
682
  const result = await executeVault({
602
683
  action: "delete",
@@ -607,7 +688,7 @@ describe("credential_store tool", () => {
607
688
  expect(result.content).toBe("Deleted credential for gmail/password.");
608
689
 
609
690
  // Verify it's actually gone
610
- expect(getSecureKey("credential:gmail:password")).toBeUndefined();
691
+ expect(getSecureKey(credentialKey("gmail", "password"))).toBeUndefined();
611
692
  });
612
693
 
613
694
  test("returns error for non-existent credential", async () => {
@@ -637,6 +718,44 @@ describe("credential_store tool", () => {
637
718
  expect(result.isError).toBe(true);
638
719
  expect(result.content).toContain("field is required");
639
720
  });
721
+
722
+ test("delete also disconnects OAuth connection for the service", async () => {
723
+ // Store a credential via the real tool so metadata exists
724
+ await credentialStoreTool.execute(
725
+ {
726
+ action: "store",
727
+ service: "integration:gmail",
728
+ field: "api_key",
729
+ value: "test-value",
730
+ },
731
+ _ctx,
732
+ );
733
+
734
+ // Simulate an active OAuth connection for this service
735
+ mockConnections.set("integration:gmail", {
736
+ id: "conn-gmail",
737
+ providerKey: "integration:gmail",
738
+ oauthAppId: "app-gmail",
739
+ expiresAt: Date.now() + 3600_000,
740
+ });
741
+
742
+ const result = await credentialStoreTool.execute(
743
+ {
744
+ action: "delete",
745
+ service: "integration:gmail",
746
+ field: "api_key",
747
+ },
748
+ _ctx,
749
+ );
750
+
751
+ expect(result.isError).toBe(false);
752
+ expect(result.content).toContain("Deleted credential");
753
+ // Verify disconnectOAuthProvider was called with the service name
754
+ expect(mockDisconnectOAuthProvider).toHaveBeenCalledTimes(1);
755
+ expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith(
756
+ "integration:gmail",
757
+ );
758
+ });
640
759
  });
641
760
 
642
761
  // -----------------------------------------------------------------------
@@ -644,12 +763,14 @@ describe("credential_store tool", () => {
644
763
  // -----------------------------------------------------------------------
645
764
  describe("credential value access", () => {
646
765
  test("credential values are stored via secure keys", () => {
647
- setSecureKey("credential:github:token", "ghp_abc123");
648
- expect(getSecureKey("credential:github:token")).toBe("ghp_abc123");
766
+ setSecureKey(credentialKey("github", "token"), "ghp_abc123");
767
+ expect(getSecureKey(credentialKey("github", "token"))).toBe("ghp_abc123");
649
768
  });
650
769
 
651
770
  test("returns undefined for non-existent credential", () => {
652
- expect(getSecureKey("credential:nonexistent:field")).toBeUndefined();
771
+ expect(
772
+ getSecureKey(credentialKey("nonexistent", "field")),
773
+ ).toBeUndefined();
653
774
  });
654
775
  });
655
776
 
@@ -1094,8 +1215,12 @@ describe("credential_store tool", () => {
1094
1215
  value: "github-pass",
1095
1216
  });
1096
1217
 
1097
- expect(getSecureKey("credential:gmail:password")).toBe("gmail-pass");
1098
- expect(getSecureKey("credential:github:password")).toBe("github-pass");
1218
+ expect(getSecureKey(credentialKey("gmail", "password"))).toBe(
1219
+ "gmail-pass",
1220
+ );
1221
+ expect(getSecureKey(credentialKey("github", "password"))).toBe(
1222
+ "github-pass",
1223
+ );
1099
1224
  });
1100
1225
 
1101
1226
  test("same service with different fields do not collide", async () => {
@@ -1112,10 +1237,279 @@ describe("credential_store tool", () => {
1112
1237
  value: "backup@example.com",
1113
1238
  });
1114
1239
 
1115
- expect(getSecureKey("credential:gmail:password")).toBe("pass123");
1116
- expect(getSecureKey("credential:gmail:recovery_email")).toBe(
1240
+ expect(getSecureKey(credentialKey("gmail", "password"))).toBe("pass123");
1241
+ expect(getSecureKey(credentialKey("gmail", "recovery_email"))).toBe(
1117
1242
  "backup@example.com",
1118
1243
  );
1119
1244
  });
1120
1245
  });
1121
1246
  });
1247
+
1248
+ // ---------------------------------------------------------------------------
1249
+ // Token refresh deduplication tests
1250
+ // ---------------------------------------------------------------------------
1251
+
1252
+ describe("withValidToken refresh deduplication", () => {
1253
+ beforeAll(() => {
1254
+ mkdirSync(TEST_DIR, { recursive: true });
1255
+ });
1256
+
1257
+ beforeEach(() => {
1258
+ _resetBackend();
1259
+ for (const entry of readdirSync(TEST_DIR)) {
1260
+ rmSync(join(TEST_DIR, entry), { recursive: true, force: true });
1261
+ }
1262
+ _setStorePath(STORE_PATH);
1263
+ _setMetadataPath(join(TEST_DIR, "metadata.json"));
1264
+ _resetRefreshBreakers();
1265
+ _resetInflightRefreshes();
1266
+ mockRefreshOAuth2Token.mockClear();
1267
+ // Clear mock oauth-store maps
1268
+ mockConnections.clear();
1269
+ mockApps.clear();
1270
+ mockProviders.clear();
1271
+ });
1272
+
1273
+ afterEach(() => {
1274
+ _setMetadataPath(null);
1275
+ _setStorePath(null);
1276
+ _resetBackend();
1277
+ _resetRefreshBreakers();
1278
+ _resetInflightRefreshes();
1279
+ mockConnections.clear();
1280
+ mockApps.clear();
1281
+ mockProviders.clear();
1282
+ });
1283
+
1284
+ afterAll(() => {
1285
+ rmSync(TEST_DIR, { recursive: true, force: true });
1286
+ });
1287
+
1288
+ /**
1289
+ * Helper: set up a service with an access token, refresh token, and
1290
+ * mock DB data so that token refresh can proceed through doRefresh().
1291
+ *
1292
+ * OAuth-specific fields (tokenUrl, clientId, expiresAt) are now stored
1293
+ * in the SQLite oauth-store. The mock maps simulate the DB layer.
1294
+ */
1295
+ function setupService(
1296
+ service: string,
1297
+ opts?: { expired?: boolean; accessToken?: string },
1298
+ ) {
1299
+ const accessToken = opts?.accessToken ?? "old-access-token";
1300
+
1301
+ // Seed mock oauth-store maps so token-manager can resolve refresh config
1302
+ const appId = `app-${service}`;
1303
+ const connId = `conn-${service}`;
1304
+
1305
+ // Store access token under the oauth_connection key path that
1306
+ // withValidToken reads (not the legacy credentialKey path).
1307
+ setSecureKey(`oauth_connection/${connId}/access_token`, accessToken);
1308
+ mockProviders.set(service, {
1309
+ key: service,
1310
+ tokenUrl: "https://oauth.example.com/token",
1311
+ });
1312
+ mockApps.set(appId, {
1313
+ id: appId,
1314
+ providerKey: service,
1315
+ clientId: "test-client-id",
1316
+ });
1317
+ mockConnections.set(service, {
1318
+ id: connId,
1319
+ providerKey: service,
1320
+ oauthAppId: appId,
1321
+ expiresAt: opts?.expired
1322
+ ? Date.now() - 60_000 // expired 1 minute ago
1323
+ : Date.now() + 3600_000, // expires in 1 hour
1324
+ });
1325
+ // Store refresh token and client_secret in secure keys (token-manager reads them)
1326
+ setSecureKey(
1327
+ `oauth_connection/${connId}/refresh_token`,
1328
+ "valid-refresh-token",
1329
+ );
1330
+ setSecureKey(`oauth_app/${appId}/client_secret`, "test-client-secret");
1331
+ }
1332
+
1333
+ test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
1334
+ setupService("integration:gmail");
1335
+
1336
+ let resolveRefresh!: (value: {
1337
+ accessToken: string;
1338
+ expiresIn: number;
1339
+ }) => void;
1340
+ const refreshPromise = new Promise<{
1341
+ accessToken: string;
1342
+ expiresIn: number;
1343
+ }>((resolve) => {
1344
+ resolveRefresh = resolve;
1345
+ });
1346
+
1347
+ mockRefreshOAuth2Token.mockImplementation(() => refreshPromise);
1348
+
1349
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
1350
+
1351
+ const callback = async (token: string) => {
1352
+ if (token === "old-access-token") throw err401;
1353
+ return `result-with-${token}`;
1354
+ };
1355
+
1356
+ // Launch 3 concurrent withValidToken calls — all will get a non-expired
1357
+ // token first, call the callback, get a 401, and then try to refresh.
1358
+ const p1 = withValidToken("integration:gmail", callback);
1359
+ const p2 = withValidToken("integration:gmail", callback);
1360
+ const p3 = withValidToken("integration:gmail", callback);
1361
+
1362
+ // Let the event loop tick so all 3 calls enter the 401 retry path
1363
+ await new Promise((r) => setTimeout(r, 10));
1364
+
1365
+ // Resolve the single refresh attempt
1366
+ resolveRefresh({ accessToken: "new-token-123", expiresIn: 3600 });
1367
+
1368
+ const results = await Promise.all([p1, p2, p3]);
1369
+
1370
+ // All 3 should succeed with the refreshed token
1371
+ expect(results).toEqual([
1372
+ "result-with-new-token-123",
1373
+ "result-with-new-token-123",
1374
+ "result-with-new-token-123",
1375
+ ]);
1376
+
1377
+ // refreshOAuth2Token should have been called exactly once
1378
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
1379
+ });
1380
+
1381
+ test("concurrent refreshes for different services proceed independently", async () => {
1382
+ setupService("integration:gmail");
1383
+ setupService("integration:slack");
1384
+
1385
+ let resolveGmail!: (value: {
1386
+ accessToken: string;
1387
+ expiresIn: number;
1388
+ }) => void;
1389
+ let resolveSlack!: (value: {
1390
+ accessToken: string;
1391
+ expiresIn: number;
1392
+ }) => void;
1393
+
1394
+ const gmailPromise = new Promise<{
1395
+ accessToken: string;
1396
+ expiresIn: number;
1397
+ }>((resolve) => {
1398
+ resolveGmail = resolve;
1399
+ });
1400
+ const slackPromise = new Promise<{
1401
+ accessToken: string;
1402
+ expiresIn: number;
1403
+ }>((resolve) => {
1404
+ resolveSlack = resolve;
1405
+ });
1406
+
1407
+ let refreshCallCount = 0;
1408
+ mockRefreshOAuth2Token.mockImplementation(() => {
1409
+ refreshCallCount++;
1410
+ // Both services use the same tokenUrl in this test, so we track by
1411
+ // call order to return the correct deferred promise.
1412
+ if (refreshCallCount === 1) return gmailPromise;
1413
+ return slackPromise;
1414
+ });
1415
+
1416
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
1417
+
1418
+ const gmailCallback = async (token: string) => {
1419
+ if (token === "old-access-token") throw err401;
1420
+ return `gmail-${token}`;
1421
+ };
1422
+ const slackCallback = async (token: string) => {
1423
+ if (token === "old-access-token") throw err401;
1424
+ return `slack-${token}`;
1425
+ };
1426
+
1427
+ const p1 = withValidToken("integration:gmail", gmailCallback);
1428
+ const p2 = withValidToken("integration:slack", slackCallback);
1429
+
1430
+ await new Promise((r) => setTimeout(r, 10));
1431
+
1432
+ // Resolve both independently
1433
+ resolveGmail({ accessToken: "gmail-new-token", expiresIn: 3600 });
1434
+ resolveSlack({ accessToken: "slack-new-token", expiresIn: 3600 });
1435
+
1436
+ const [r1, r2] = await Promise.all([p1, p2]);
1437
+
1438
+ expect(r1).toBe("gmail-gmail-new-token");
1439
+ expect(r2).toBe("slack-slack-new-token");
1440
+
1441
+ // Both services should have triggered their own refresh
1442
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(2);
1443
+ });
1444
+
1445
+ test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
1446
+ setupService("integration:gmail");
1447
+
1448
+ let refreshCount = 0;
1449
+ mockRefreshOAuth2Token.mockImplementation(() => {
1450
+ refreshCount++;
1451
+ return Promise.resolve({
1452
+ accessToken: `token-${refreshCount}`,
1453
+ expiresIn: 3600,
1454
+ });
1455
+ });
1456
+
1457
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
1458
+
1459
+ // First call triggers a refresh (old token → 401 → refresh → token-1)
1460
+ const r1 = await withValidToken(
1461
+ "integration:gmail",
1462
+ async (token: string) => {
1463
+ if (token !== "token-1") throw err401;
1464
+ return token;
1465
+ },
1466
+ );
1467
+ expect(r1).toBe("token-1");
1468
+ expect(refreshCount).toBe(1);
1469
+
1470
+ // Second call also triggers a 401 to verify dedup state was cleaned up
1471
+ // and a new refresh is allowed (not deduplicated with the first).
1472
+ const r2 = await withValidToken(
1473
+ "integration:gmail",
1474
+ async (token: string) => {
1475
+ if (token !== "token-2") throw err401;
1476
+ return token;
1477
+ },
1478
+ );
1479
+ expect(r2).toBe("token-2");
1480
+ // Second refresh should have happened (not deduplicated with the first,
1481
+ // since the first already completed)
1482
+ expect(refreshCount).toBe(2);
1483
+ });
1484
+
1485
+ test("deduplication propagates refresh errors to all waiting callers", async () => {
1486
+ setupService("integration:gmail");
1487
+
1488
+ mockRefreshOAuth2Token.mockImplementation(() =>
1489
+ Promise.reject(
1490
+ Object.assign(
1491
+ new Error("OAuth2 token refresh failed (HTTP 401: invalid_grant)"),
1492
+ ),
1493
+ ),
1494
+ );
1495
+
1496
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
1497
+
1498
+ const callback = async (token: string) => {
1499
+ if (token === "old-access-token") throw err401;
1500
+ return "should-not-reach";
1501
+ };
1502
+
1503
+ // Launch 2 concurrent calls — both should fail with the same error
1504
+ const p1 = withValidToken("integration:gmail", callback);
1505
+ const p2 = withValidToken("integration:gmail", callback);
1506
+
1507
+ const results = await Promise.allSettled([p1, p2]);
1508
+
1509
+ expect(results[0].status).toBe("rejected");
1510
+ expect(results[1].status).toBe("rejected");
1511
+
1512
+ // Only one actual refresh attempt
1513
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
1514
+ });
1515
+ });
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import { Command } from "commander";
4
4
 
5
+ import { credentialKey } from "../security/credential-key.js";
5
6
  import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
6
7
 
7
8
  // ---------------------------------------------------------------------------
@@ -163,6 +164,26 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
163
164
  },
164
165
  }));
165
166
 
167
+ // ---------------------------------------------------------------------------
168
+ // Mock oauth-store
169
+ // ---------------------------------------------------------------------------
170
+
171
+ let disconnectOAuthProviderCalls: string[] = [];
172
+ let disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
173
+ "not-found";
174
+
175
+ mock.module("../oauth/oauth-store.js", () => ({
176
+ disconnectOAuthProvider: async (
177
+ providerKey: string,
178
+ ): Promise<"disconnected" | "not-found" | "error"> => {
179
+ disconnectOAuthProviderCalls.push(providerKey);
180
+ return disconnectOAuthProviderResult;
181
+ },
182
+ getConnectionByProvider: (): undefined => undefined,
183
+ listConnections: (): never[] => [],
184
+ deleteConnection: (): boolean => false,
185
+ }));
186
+
166
187
  // ---------------------------------------------------------------------------
167
188
  // Import the module under test (after mocks are registered)
168
189
  // ---------------------------------------------------------------------------
@@ -238,7 +259,7 @@ function seedCredential(
238
259
  ...extra,
239
260
  };
240
261
  metadataStore.push(record);
241
- secureKeyStore.set(`credential:${service}:${field}`, secret);
262
+ secureKeyStore.set(credentialKey(service, field), secret);
242
263
  return record;
243
264
  }
244
265
 
@@ -280,6 +301,8 @@ describe("assistant credentials CLI", () => {
280
301
  _listMetadataCalls = 0;
281
302
  _getMetadataCalls = 0;
282
303
  _getMetadataByIdCalls = 0;
304
+ disconnectOAuthProviderCalls = [];
305
+ disconnectOAuthProviderResult = "not-found";
283
306
  process.exitCode = 0;
284
307
  });
285
308
 
@@ -437,7 +460,7 @@ describe("assistant credentials CLI", () => {
437
460
  expect(parsed.credentialId).toBeTruthy();
438
461
 
439
462
  // Verify secret stored in mock map
440
- expect(secureKeyStore.get("credential:twilio:account_sid")).toBe(
463
+ expect(secureKeyStore.get(credentialKey("twilio", "account_sid"))).toBe(
441
464
  "AC1234567890",
442
465
  );
443
466
 
@@ -546,7 +569,7 @@ describe("assistant credentials CLI", () => {
546
569
  expect(meta2!.updatedAt).toBeGreaterThan(firstUpdatedAt);
547
570
 
548
571
  // Verify secret is overwritten
549
- expect(secureKeyStore.get("credential:twilio:account_sid")).toBe(
572
+ expect(secureKeyStore.get(credentialKey("twilio", "account_sid"))).toBe(
550
573
  "new_value",
551
574
  );
552
575
  });
@@ -568,7 +591,9 @@ describe("assistant credentials CLI", () => {
568
591
  expect(parsed.field).toBe("auth_token");
569
592
 
570
593
  // Verify both removed
571
- expect(secureKeyStore.has("credential:twilio:auth_token")).toBe(false);
594
+ expect(secureKeyStore.has(credentialKey("twilio", "auth_token"))).toBe(
595
+ false,
596
+ );
572
597
  expect(
573
598
  metadataStore.find(
574
599
  (m) => m.service === "twilio" && m.field === "auth_token",
@@ -608,6 +633,55 @@ describe("assistant credentials CLI", () => {
608
633
  ),
609
634
  ).toBeUndefined();
610
635
  });
636
+
637
+ test("calls disconnectOAuthProvider for OAuth cleanup", async () => {
638
+ seedCredential("gmail", "access_token", "ya29.token_value");
639
+
640
+ const result = await runCli(["delete", "gmail:access_token", "--json"]);
641
+ expect(result.exitCode).toBe(0);
642
+ const parsed = JSON.parse(result.stdout);
643
+ expect(parsed.ok).toBe(true);
644
+
645
+ // disconnectOAuthProvider should have been called with the service name
646
+ expect(disconnectOAuthProviderCalls).toEqual(["gmail"]);
647
+ });
648
+
649
+ test("succeeds when only OAuth connection exists (no legacy credential)", async () => {
650
+ // No legacy credential seeded — only the OAuth disconnect finds something
651
+ disconnectOAuthProviderResult = "disconnected";
652
+
653
+ const result = await runCli(["delete", "gmail:access_token", "--json"]);
654
+ expect(result.exitCode).toBe(0);
655
+ const parsed = JSON.parse(result.stdout);
656
+ expect(parsed.ok).toBe(true);
657
+ expect(parsed.service).toBe("gmail");
658
+ expect(parsed.field).toBe("access_token");
659
+
660
+ expect(disconnectOAuthProviderCalls).toEqual(["gmail"]);
661
+ });
662
+
663
+ test("demonstrates colon-in-service-name parsing limitation with integration:gmail", async () => {
664
+ // parseCredentialName("integration:gmail:access_token") splits on the
665
+ // first colon, yielding service="integration" and field="gmail:access_token".
666
+ // This is incorrect for the intended service "integration:gmail". The fix
667
+ // for this limitation is addressed by introducing a dedicated `disconnect`
668
+ // subcommand (PR 5).
669
+ const result = await runCli([
670
+ "delete",
671
+ "integration:gmail:access_token",
672
+ "--json",
673
+ ]);
674
+ // The command parses as service="integration", field="gmail:access_token"
675
+ // which finds nothing and reports not-found.
676
+ expect(result.exitCode).toBe(1);
677
+ const parsed = JSON.parse(result.stdout);
678
+ expect(parsed.ok).toBe(false);
679
+ expect(parsed.error).toContain("not found");
680
+
681
+ // disconnectOAuthProvider was called with "integration" (wrong) instead
682
+ // of "integration:gmail" (intended).
683
+ expect(disconnectOAuthProviderCalls).toEqual(["integration"]);
684
+ });
611
685
  });
612
686
 
613
687
  // =========================================================================
@@ -833,8 +907,10 @@ describe("assistant credentials CLI", () => {
833
907
  expect(parsed.value).toBe("instance_secret_abc123");
834
908
 
835
909
  // Verify the correct key was looked up in the secure store
836
- expect(secureKeyStore.has("credential:twilio:auth_token")).toBe(true);
837
- expect(secureKeyStore.get("credential:twilio:auth_token")).toBe(
910
+ expect(secureKeyStore.has(credentialKey("twilio", "auth_token"))).toBe(
911
+ true,
912
+ );
913
+ expect(secureKeyStore.get(credentialKey("twilio", "auth_token"))).toBe(
838
914
  "instance_secret_abc123",
839
915
  );
840
916
  });
@@ -46,7 +46,6 @@ mock.module("../util/logger.js", () => ({
46
46
  ...realLogger,
47
47
  getLogger: () => noopLogger,
48
48
  getCliLogger: () => noopLogger,
49
- isDebug: () => false,
50
49
  truncateForLog: (v: string) => v,
51
50
  initLogger: () => {},
52
51
  pruneOldLogFiles: () => 0,
@@ -328,11 +328,11 @@ describe("ephemeral-permissions", () => {
328
328
  testConfig.permissions.mode = "workspace";
329
329
  });
330
330
 
331
- test("workspace mode auto-allows workspace-scoped file_write (medium risk)", async () => {
331
+ test("workspace mode prompts for workspace-scoped file_write (medium risk)", async () => {
332
332
  const filePath = join(testDir, "workspace-test-file.txt");
333
333
  const result = await check("file_write", { path: filePath }, testDir);
334
- expect(result.decision).toBe("allow");
335
- expect(result.reason).toContain("Workspace mode");
334
+ expect(result.decision).toBe("prompt");
335
+ expect(result.reason).toContain("medium risk");
336
336
  });
337
337
 
338
338
  test("workspace mode still prompts for file_write outside workspace", async () => {