@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
@@ -294,6 +294,38 @@ describe("credential metadata store", () => {
294
294
  });
295
295
  });
296
296
 
297
+ // ── v5 Schema: OAuth fields removed ─────────────────────────────────
298
+
299
+ describe("v5 schema — OAuth fields removed from CredentialMetadata", () => {
300
+ test("CredentialMetadata does not include hasRefreshToken", () => {
301
+ const record = upsertCredentialMetadata("github", "access_token", {
302
+ allowedTools: ["api_request"],
303
+ });
304
+ // hasRefreshToken was removed in v5 — field should not exist
305
+ expect("hasRefreshToken" in record).toBe(false);
306
+ });
307
+
308
+ test("CredentialMetadata does not include oauth2TokenUrl", () => {
309
+ const record = upsertCredentialMetadata("github", "access_token");
310
+ expect("oauth2TokenUrl" in record).toBe(false);
311
+ });
312
+
313
+ test("CredentialMetadata does not include oauth2ClientId", () => {
314
+ const record = upsertCredentialMetadata("github", "access_token");
315
+ expect("oauth2ClientId" in record).toBe(false);
316
+ });
317
+
318
+ test("CredentialMetadata does not include expiresAt", () => {
319
+ const record = upsertCredentialMetadata("github", "access_token");
320
+ expect("expiresAt" in record).toBe(false);
321
+ });
322
+
323
+ test("CredentialMetadata does not include grantedScopes", () => {
324
+ const record = upsertCredentialMetadata("github", "access_token");
325
+ expect("grantedScopes" in record).toBe(false);
326
+ });
327
+ });
328
+
297
329
  // ── Version migration ──────────────────────────────────────────────
298
330
 
299
331
  describe("version migration", () => {
@@ -371,7 +403,7 @@ describe("credential metadata store", () => {
371
403
  expect(record!.credentialId).toBe("cred-stable-id");
372
404
  });
373
405
 
374
- test("v2 file is loaded without migration", () => {
406
+ test("v2 file is migrated to v5 (strips oauth2ClientSecret and OAuth fields)", () => {
375
407
  const v2Data = {
376
408
  version: 2,
377
409
  credentials: [
@@ -382,6 +414,7 @@ describe("credential metadata store", () => {
382
414
  allowedTools: ["api_request"],
383
415
  allowedDomains: ["fal.ai"],
384
416
  alias: "fal-primary",
417
+ oauth2ClientSecret: "should-be-stripped",
385
418
  injectionTemplates: [
386
419
  {
387
420
  hostPattern: "*.fal.ai",
@@ -402,9 +435,191 @@ describe("credential metadata store", () => {
402
435
  expect(record!.alias).toBe("fal-primary");
403
436
  expect(record!.injectionTemplates).toHaveLength(1);
404
437
  expect(record!.injectionTemplates![0].hostPattern).toBe("*.fal.ai");
438
+ // oauth2ClientSecret must be stripped by migration
439
+ expect("oauth2ClientSecret" in record!).toBe(false);
440
+
441
+ // On-disk file should be upgraded to v5
442
+ const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
443
+ expect(raw.version).toBe(5);
444
+ expect(raw.credentials[0]).not.toHaveProperty("oauth2ClientSecret");
445
+ });
446
+
447
+ test("v3 file is migrated to v5 (removes ghost refresh_token records)", () => {
448
+ const v3Data = {
449
+ version: 3,
450
+ credentials: [
451
+ {
452
+ credentialId: "cred-v3-at",
453
+ service: "github",
454
+ field: "access_token",
455
+ allowedTools: ["api_request"],
456
+ allowedDomains: ["github.com"],
457
+ createdAt: 1700000000000,
458
+ updatedAt: 1700000000000,
459
+ },
460
+ {
461
+ credentialId: "cred-v3-rt",
462
+ service: "github",
463
+ field: "refresh_token",
464
+ allowedTools: [],
465
+ allowedDomains: [],
466
+ createdAt: 1700000000000,
467
+ updatedAt: 1700000000000,
468
+ },
469
+ ],
470
+ };
471
+ writeFileSync(META_PATH, JSON.stringify(v3Data, null, 2), "utf-8");
472
+
473
+ const records = listCredentialMetadata();
474
+ // Ghost refresh_token record removed
475
+ expect(records).toHaveLength(1);
476
+ expect(records[0].field).toBe("access_token");
477
+ // hasRefreshToken is stripped in v5 migration (OAuth fields moved to SQLite)
478
+ expect("hasRefreshToken" in records[0]).toBe(false);
479
+
480
+ // On-disk file should be upgraded to v5
481
+ const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
482
+ expect(raw.version).toBe(5);
483
+ expect(raw.credentials).toHaveLength(1);
484
+ expect(raw.credentials[0].field).toBe("access_token");
405
485
  });
406
486
 
407
- test("future version (v3+) returns unknown version and refuses writes", () => {
487
+ test("v3 file without refresh_token records migrates cleanly", () => {
488
+ const v3Data = {
489
+ version: 3,
490
+ credentials: [
491
+ {
492
+ credentialId: "cred-v3-001",
493
+ service: "fal-ai",
494
+ field: "api_key",
495
+ allowedTools: ["api_request"],
496
+ allowedDomains: ["fal.ai"],
497
+ alias: "fal-primary",
498
+ injectionTemplates: [
499
+ {
500
+ hostPattern: "*.fal.ai",
501
+ injectionType: "header",
502
+ headerName: "Authorization",
503
+ valuePrefix: "Key ",
504
+ },
505
+ ],
506
+ createdAt: 1700000000000,
507
+ updatedAt: 1700000000000,
508
+ },
509
+ ],
510
+ };
511
+ writeFileSync(META_PATH, JSON.stringify(v3Data, null, 2), "utf-8");
512
+
513
+ const record = getCredentialMetadata("fal-ai", "api_key");
514
+ expect(record).toBeDefined();
515
+ expect(record!.alias).toBe("fal-primary");
516
+ expect(record!.injectionTemplates).toHaveLength(1);
517
+ expect(record!.injectionTemplates![0].hostPattern).toBe("*.fal.ai");
518
+ // hasRefreshToken is stripped in v5 migration
519
+ expect("hasRefreshToken" in record!).toBe(false);
520
+
521
+ // On-disk file should be upgraded to v5
522
+ const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
523
+ expect(raw.version).toBe(5);
524
+ });
525
+
526
+ test("v3 migration handles multiple services with ghost records", () => {
527
+ const v3Data = {
528
+ version: 3,
529
+ credentials: [
530
+ {
531
+ credentialId: "at-github",
532
+ service: "github",
533
+ field: "access_token",
534
+ allowedTools: [],
535
+ allowedDomains: [],
536
+ createdAt: 1700000000000,
537
+ updatedAt: 1700000000000,
538
+ },
539
+ {
540
+ credentialId: "rt-github",
541
+ service: "github",
542
+ field: "refresh_token",
543
+ allowedTools: [],
544
+ allowedDomains: [],
545
+ createdAt: 1700000000000,
546
+ updatedAt: 1700000000000,
547
+ },
548
+ {
549
+ credentialId: "at-slack",
550
+ service: "slack",
551
+ field: "access_token",
552
+ allowedTools: [],
553
+ allowedDomains: [],
554
+ createdAt: 1700000000000,
555
+ updatedAt: 1700000000000,
556
+ },
557
+ {
558
+ credentialId: "at-stripe",
559
+ service: "stripe",
560
+ field: "access_token",
561
+ allowedTools: [],
562
+ allowedDomains: [],
563
+ createdAt: 1700000000000,
564
+ updatedAt: 1700000000000,
565
+ },
566
+ {
567
+ credentialId: "rt-stripe",
568
+ service: "stripe",
569
+ field: "refresh_token",
570
+ allowedTools: [],
571
+ allowedDomains: [],
572
+ createdAt: 1700000000000,
573
+ updatedAt: 1700000000000,
574
+ },
575
+ ],
576
+ };
577
+ writeFileSync(META_PATH, JSON.stringify(v3Data, null, 2), "utf-8");
578
+
579
+ const records = listCredentialMetadata();
580
+ // refresh_token records removed, only access_token records remain
581
+ expect(records).toHaveLength(3);
582
+ expect(records.every((r) => r.field !== "refresh_token")).toBe(true);
583
+ // hasRefreshToken is stripped in v5 migration — none should have it
584
+ const github = records.find((r) => r.service === "github");
585
+ const slack = records.find((r) => r.service === "slack");
586
+ const stripe = records.find((r) => r.service === "stripe");
587
+ expect("hasRefreshToken" in github!).toBe(false);
588
+ expect("hasRefreshToken" in slack!).toBe(false);
589
+ expect("hasRefreshToken" in stripe!).toBe(false);
590
+ });
591
+
592
+ test("v4 file is migrated to v5 (strips hasRefreshToken and OAuth fields)", () => {
593
+ const v4Data = {
594
+ version: 4,
595
+ credentials: [
596
+ {
597
+ credentialId: "cred-v4-001",
598
+ service: "fal-ai",
599
+ field: "api_key",
600
+ allowedTools: ["api_request"],
601
+ allowedDomains: ["fal.ai"],
602
+ alias: "fal-primary",
603
+ hasRefreshToken: true,
604
+ createdAt: 1700000000000,
605
+ updatedAt: 1700000000000,
606
+ },
607
+ ],
608
+ };
609
+ writeFileSync(META_PATH, JSON.stringify(v4Data, null, 2), "utf-8");
610
+
611
+ const record = getCredentialMetadata("fal-ai", "api_key");
612
+ expect(record).toBeDefined();
613
+ expect(record!.alias).toBe("fal-primary");
614
+ // hasRefreshToken is stripped in v5 migration
615
+ expect("hasRefreshToken" in record!).toBe(false);
616
+
617
+ // On-disk file should be upgraded to v5
618
+ const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
619
+ expect(raw.version).toBe(5);
620
+ });
621
+
622
+ test("future version (v6+) returns unknown version and refuses writes", () => {
408
623
  const futureData = {
409
624
  version: 99,
410
625
  credentials: [],
@@ -439,7 +654,7 @@ describe("credential metadata store", () => {
439
654
  },
440
655
  );
441
656
 
442
- test("upsert on migrated v1 file saves as v2", () => {
657
+ test("upsert on migrated v1 file saves as v5", () => {
443
658
  const v1Data = {
444
659
  version: 1,
445
660
  credentials: [
@@ -459,13 +674,13 @@ describe("credential metadata store", () => {
459
674
  // Upsert triggers load (migration) + save (at current version)
460
675
  upsertCredentialMetadata("github", "token", { alias: "gh-main" });
461
676
 
462
- // Verify on-disk file is now v2
677
+ // Verify on-disk file is now v5
463
678
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
464
- expect(raw.version).toBe(2);
679
+ expect(raw.version).toBe(5);
465
680
  expect(raw.credentials[0].alias).toBe("gh-main");
466
681
  });
467
682
 
468
- test("v1 load auto-persists as v2 on disk without requiring a write", () => {
683
+ test("v1 load auto-persists as v5 on disk without requiring a write", () => {
469
684
  const v1Data = {
470
685
  version: 1,
471
686
  credentials: [
@@ -482,15 +697,15 @@ describe("credential metadata store", () => {
482
697
  };
483
698
  writeFileSync(META_PATH, JSON.stringify(v1Data, null, 2), "utf-8");
484
699
 
485
- // A read-only operation should still persist the v2 upgrade
700
+ // A read-only operation should still persist the v5 upgrade
486
701
  listCredentialMetadata();
487
702
 
488
703
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
489
- expect(raw.version).toBe(2);
704
+ expect(raw.version).toBe(5);
490
705
  expect(raw.credentials[0].credentialId).toBe("cred-autopersist");
491
706
  });
492
707
 
493
- test("v1 file with multiple credentials migrates all records", () => {
708
+ test("v1 file with multiple credentials migrates all records (strips OAuth fields)", () => {
494
709
  const v1Data = {
495
710
  version: 1,
496
711
  credentials: [
@@ -539,13 +754,11 @@ describe("credential metadata store", () => {
539
754
  expect(r.injectionTemplates).toBeUndefined();
540
755
  }
541
756
 
542
- // Original v1 fields preserved
543
- expect(records[0].grantedScopes).toEqual(["repo", "user"]);
544
- expect(records[1].expiresAt).toBe(1800000000000);
545
- expect(records[2].oauth2TokenUrl).toBe(
546
- "https://connect.stripe.com/oauth/token",
547
- );
548
- expect(records[2].oauth2ClientId).toBe("ca_test123");
757
+ // OAuth-specific fields are stripped by v5 migration
758
+ expect("grantedScopes" in records[0]).toBe(false);
759
+ expect("expiresAt" in records[1]).toBe(false);
760
+ expect("oauth2TokenUrl" in records[2]).toBe(false);
761
+ expect("oauth2ClientId" in records[2]).toBe(false);
549
762
  });
550
763
  });
551
764
 
@@ -586,20 +799,20 @@ describe("credential metadata store", () => {
586
799
  test("file with non-array credentials field is treated as empty list", () => {
587
800
  writeFileSync(
588
801
  META_PATH,
589
- JSON.stringify({ version: 2, credentials: "not-an-array" }),
802
+ JSON.stringify({ version: 5, credentials: "not-an-array" }),
590
803
  "utf-8",
591
804
  );
592
805
  expect(listCredentialMetadata()).toEqual([]);
593
806
  });
594
807
 
595
808
  test("file with missing credentials field is treated as empty list", () => {
596
- writeFileSync(META_PATH, JSON.stringify({ version: 2 }), "utf-8");
809
+ writeFileSync(META_PATH, JSON.stringify({ version: 5 }), "utf-8");
597
810
  expect(listCredentialMetadata()).toEqual([]);
598
811
  });
599
812
 
600
813
  test("malformed records within credentials array are filtered out", () => {
601
814
  const data = {
602
- version: 2,
815
+ version: 5,
603
816
  credentials: [
604
817
  // Valid record
605
818
  {
@@ -709,7 +922,7 @@ describe("credential metadata store", () => {
709
922
 
710
923
  const raw = readFileSync(META_PATH, "utf-8");
711
924
  const parsed = JSON.parse(raw);
712
- expect(parsed.version).toBe(2);
925
+ expect(parsed.version).toBe(5);
713
926
  expect(parsed.credentials).toHaveLength(1);
714
927
  expect(parsed.credentials[0].service).toBe("slack");
715
928
  });
@@ -717,23 +930,23 @@ describe("credential metadata store", () => {
717
930
  test("file written by saveFile has version field", () => {
718
931
  upsertCredentialMetadata("test", "key");
719
932
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
720
- expect(raw.version).toBe(2);
933
+ expect(raw.version).toBe(5);
721
934
  });
722
935
  });
723
936
 
724
937
  // ── Empty credential lists ────────────────────────────────────────
725
938
 
726
939
  describe("empty credential lists", () => {
727
- test("empty v2 file returns empty array", () => {
940
+ test("empty v5 file returns empty array", () => {
728
941
  writeFileSync(
729
942
  META_PATH,
730
- JSON.stringify({ version: 2, credentials: [] }, null, 2),
943
+ JSON.stringify({ version: 5, credentials: [] }, null, 2),
731
944
  "utf-8",
732
945
  );
733
946
  expect(listCredentialMetadata()).toEqual([]);
734
947
  });
735
948
 
736
- test("empty v1 file is migrated to v2 with empty credentials", () => {
949
+ test("empty v1 file is migrated to v5 with empty credentials", () => {
737
950
  writeFileSync(
738
951
  META_PATH,
739
952
  JSON.stringify({ version: 1, credentials: [] }, null, 2),
@@ -741,9 +954,9 @@ describe("credential metadata store", () => {
741
954
  );
742
955
  expect(listCredentialMetadata()).toEqual([]);
743
956
 
744
- // Should be persisted as v2
957
+ // Should be persisted as v5
745
958
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
746
- expect(raw.version).toBe(2);
959
+ expect(raw.version).toBe(5);
747
960
  expect(raw.credentials).toEqual([]);
748
961
  });
749
962
 
@@ -12,6 +12,7 @@ mock.module("../util/logger.js", () => ({
12
12
  }),
13
13
  }));
14
14
 
15
+ import { credentialKey } from "../security/credential-key.js";
15
16
  import {
16
17
  _setMetadataPath,
17
18
  upsertCredentialMetadata,
@@ -56,7 +57,7 @@ describe("credential resolver", () => {
56
57
  expect(result!.credentialId).toBe(created.credentialId);
57
58
  expect(result!.service).toBe("github");
58
59
  expect(result!.field).toBe("token");
59
- expect(result!.storageKey).toBe("credential:github:token");
60
+ expect(result!.storageKey).toBe(credentialKey("github", "token"));
60
61
  expect(result!.metadata.allowedTools).toEqual([
61
62
  "browser_fill_credential",
62
63
  ]);
@@ -111,7 +112,7 @@ describe("credential resolver", () => {
111
112
  expect(result!.credentialId).toBe(created.credentialId);
112
113
  expect(result!.service).toBe("gmail");
113
114
  expect(result!.field).toBe("password");
114
- expect(result!.storageKey).toBe("credential:gmail:password");
115
+ expect(result!.storageKey).toBe(credentialKey("gmail", "password"));
115
116
  });
116
117
 
117
118
  test("returns undefined for non-existent ID", () => {
@@ -190,10 +191,10 @@ describe("credential resolver", () => {
190
191
  });
191
192
 
192
193
  describe("storage key format", () => {
193
- test("storage key follows credential:{service}:{field} format", () => {
194
+ test("storage key follows credential/{service}/{field} format", () => {
194
195
  upsertCredentialMetadata("my-service", "api-key");
195
196
  const result = resolveByServiceField("my-service", "api-key");
196
- expect(result!.storageKey).toBe("credential:my-service:api-key");
197
+ expect(result!.storageKey).toBe(credentialKey("my-service", "api-key"));
197
198
  });
198
199
  });
199
200
 
@@ -147,12 +147,12 @@ describe("E2E: secure store and list lifecycle", () => {
147
147
  // Value must NOT appear in tool output (invariant 1)
148
148
  expect(result.content).not.toContain("ghp_abc123");
149
149
  // Value must be in keychain
150
- expect(storedKeys.get("credential:github:token")).toBe("ghp_abc123");
150
+ expect(storedKeys.get("credential/github/token")).toBe("ghp_abc123");
151
151
  });
152
152
 
153
153
  test("list returns service/field pairs without secret values", async () => {
154
- storedKeys.set("credential:github:token", "secret1");
155
- storedKeys.set("credential:aws:access_key", "secret2");
154
+ storedKeys.set("credential/github/token", "secret1");
155
+ storedKeys.set("credential/aws/access_key", "secret2");
156
156
  metadataStore.set("github:token", {
157
157
  credentialId: "cred-github-token",
158
158
  service: "github",
@@ -177,14 +177,14 @@ describe("E2E: secure store and list lifecycle", () => {
177
177
  });
178
178
 
179
179
  test("delete removes credential from keychain", async () => {
180
- storedKeys.set("credential:github:token", "secret1");
180
+ storedKeys.set("credential/github/token", "secret1");
181
181
 
182
182
  const result = await credentialStoreTool.execute(
183
183
  { action: "delete", service: "github", field: "token" },
184
184
  makeContext(),
185
185
  );
186
186
  expect(result.isError).toBe(false);
187
- expect(storedKeys.has("credential:github:token")).toBe(false);
187
+ expect(storedKeys.has("credential/github/token")).toBe(false);
188
188
  });
189
189
  });
190
190
 
@@ -267,7 +267,7 @@ describe("E2E: one-time send override", () => {
267
267
  );
268
268
  expect(result.isError).toBe(true);
269
269
  expect(result.content).toContain("not enabled");
270
- expect(storedKeys.has("credential:svc:key")).toBe(false);
270
+ expect(storedKeys.has("credential/svc/key")).toBe(false);
271
271
  });
272
272
 
273
273
  test("accepts transient_send when config gate is on", async () => {
@@ -285,7 +285,7 @@ describe("E2E: one-time send override", () => {
285
285
  expect(result.isError).toBe(false);
286
286
  expect(result.content).toContain("NOT saved");
287
287
  // Value must NOT be in keychain
288
- expect(storedKeys.has("credential:svc:key")).toBe(false);
288
+ expect(storedKeys.has("credential/svc/key")).toBe(false);
289
289
  // Value must NOT appear in output
290
290
  expect(result.content).not.toContain("tmp1");
291
291
  });
@@ -303,7 +303,7 @@ describe("E2E: one-time send override", () => {
303
303
  ctx,
304
304
  );
305
305
  expect(result.isError).toBe(false);
306
- expect(storedKeys.has("credential:svc:key")).toBe(true);
306
+ expect(storedKeys.has("credential/svc/key")).toBe(true);
307
307
  });
308
308
  });
309
309
 
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { mkdirSync, rmSync } from "node:fs";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
@@ -63,11 +63,13 @@ mock.module("../tools/registry.js", () => ({
63
63
  // ---------------------------------------------------------------------------
64
64
 
65
65
  import { DEFAULT_CONFIG } from "../config/defaults.js";
66
+ import { credentialKey } from "../security/credential-key.js";
66
67
  import { redactSensitiveFields } from "../security/redaction.js";
67
- import { setSecureKey } from "../security/secure-keys.js";
68
+ import { getSecureKey, setSecureKey } from "../security/secure-keys.js";
68
69
  import { CredentialBroker } from "../tools/credentials/broker.js";
69
70
  import {
70
71
  _setMetadataPath,
72
+ getCredentialMetadata,
71
73
  upsertCredentialMetadata,
72
74
  } from "../tools/credentials/metadata-store.js";
73
75
 
@@ -199,6 +201,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
199
201
  // Hard boundary: only these production files may import from secure-keys.
200
202
  // Any new import must be reviewed for secret-leak risk and added here.
201
203
  const ALLOWED_IMPORTERS = new Set([
204
+ "security/credential-key.ts", // credential key builder + migration
202
205
  "config/loader.ts", // config management (API keys)
203
206
  "tools/credentials/vault.ts", // credential store tool
204
207
  "tools/credentials/broker.ts", // brokered credential access
@@ -226,10 +229,13 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
226
229
  "runtime/routes/integrations/slack/share.ts", // Slack share routes credential lookup
227
230
  "mcp/client.ts", // MCP client cached-token lookup
228
231
  "oauth/token-persistence.ts", // OAuth token persistence (set/delete tokens)
232
+ "oauth/connection-resolver.ts", // resolve OAuthConnection from oauth-store (access_token lookup)
229
233
  "runtime/routes/secret-routes.ts", // HTTP secret management routes (set/delete secrets)
230
- "daemon/ride-shotgun-handler.ts", // learn session cookie persistence
231
234
  "daemon/session-messaging.ts", // credential storage during session messaging
232
- "runtime/routes/settings-routes.ts", // settings routes OAuth credential lookup (client_id/client_secret/access tokens)
235
+ "runtime/routes/settings-routes.ts", // settings routes OAuth credential lookup (client_secret)
236
+ "oauth/oauth-store.ts", // OAuth provider disconnect (delete stored tokens)
237
+ "cli/commands/oauth/connections.ts", // CLI OAuth connection delete (legacy credential cleanup)
238
+ "oauth/manual-token-connection.ts", // manual-token provider backfill (keychain credential existence check)
233
239
  ]);
234
240
 
235
241
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -245,7 +251,11 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
245
251
  const s = statSync(full);
246
252
  if (s.isDirectory()) {
247
253
  collectTsFiles(full, files);
248
- } else if (entry.endsWith(".ts") && !entry.endsWith(".d.ts")) {
254
+ } else if (
255
+ entry.endsWith(".ts") &&
256
+ !entry.endsWith(".d.ts") &&
257
+ !entry.endsWith(".test.ts")
258
+ ) {
249
259
  files.push(full);
250
260
  }
251
261
  }
@@ -413,7 +423,10 @@ describe("Invariant 4: credentials only used for allowed purpose", () => {
413
423
  allowedTools: tc.allowedTools,
414
424
  allowedDomains: tc.allowedDomains,
415
425
  });
416
- setSecureKey(`credential:${tc.credentialId}:token`, "test-secret-value");
426
+ setSecureKey(
427
+ credentialKey(tc.credentialId, "token"),
428
+ "test-secret-value",
429
+ );
417
430
 
418
431
  const result = await broker.browserFill({
419
432
  service: tc.credentialId,
@@ -438,7 +451,7 @@ describe("Invariant 4: credentials only used for allowed purpose", () => {
438
451
  allowedTools: ["browser_fill_credential"],
439
452
  allowedDomains: ["github.com"],
440
453
  });
441
- setSecureKey("credential:github:token", "ghp_secret123");
454
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
442
455
 
443
456
  const result = await broker.browserFill({
444
457
  service: "github",
@@ -470,6 +483,97 @@ describe("Invariant 4: credentials only used for allowed purpose", () => {
470
483
  });
471
484
  });
472
485
 
486
+ // ---------------------------------------------------------------------------
487
+ // Invariant 6 — oauth2ClientSecret never in plaintext metadata
488
+ // ---------------------------------------------------------------------------
489
+
490
+ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store", () => {
491
+ beforeEach(() => {
492
+ mkdirSync(TEST_DIR, { recursive: true });
493
+ _setStorePath(STORE_PATH);
494
+ _resetBackend();
495
+ _setMetadataPath(join(TEST_DIR, "metadata.json"));
496
+ });
497
+
498
+ afterEach(() => {
499
+ _setMetadataPath(null);
500
+ _setStorePath(null);
501
+ _resetBackend();
502
+ rmSync(TEST_DIR, { recursive: true, force: true });
503
+ });
504
+
505
+ test("upsertCredentialMetadata does not accept oauth2ClientSecret or other OAuth fields", () => {
506
+ const record = upsertCredentialMetadata(
507
+ "integration:gmail",
508
+ "access_token",
509
+ {
510
+ allowedTools: ["api_request"],
511
+ },
512
+ );
513
+ expect("oauth2ClientSecret" in record).toBe(false);
514
+ expect("oauth2TokenUrl" in record).toBe(false);
515
+ expect("oauth2ClientId" in record).toBe(false);
516
+ });
517
+
518
+ test("client secret is read from secure store, not metadata", () => {
519
+ setSecureKey(
520
+ credentialKey("integration:gmail", "client_secret"),
521
+ "my-secret",
522
+ );
523
+ upsertCredentialMetadata("integration:gmail", "access_token", {
524
+ allowedTools: ["api_request"],
525
+ });
526
+
527
+ const meta = getCredentialMetadata("integration:gmail", "access_token");
528
+ expect(meta).toBeDefined();
529
+ expect("oauth2ClientSecret" in meta!).toBe(false);
530
+ // OAuth-specific fields are no longer in metadata (v5)
531
+ expect("oauth2TokenUrl" in meta!).toBe(false);
532
+ expect("oauth2ClientId" in meta!).toBe(false);
533
+
534
+ // Secret is in secure store
535
+ expect(
536
+ getSecureKey(credentialKey("integration:gmail", "client_secret")),
537
+ ).toBe("my-secret");
538
+ });
539
+
540
+ test("v2 metadata with oauth2ClientSecret is stripped on migration", () => {
541
+ const v2Data = {
542
+ version: 2,
543
+ credentials: [
544
+ {
545
+ credentialId: "cred-v2-secret",
546
+ service: "integration:gmail",
547
+ field: "access_token",
548
+ allowedTools: [],
549
+ allowedDomains: [],
550
+ oauth2TokenUrl: "https://oauth2.googleapis.com/token",
551
+ oauth2ClientId: "test-client-id",
552
+ oauth2ClientSecret: "plaintext-secret-should-be-stripped",
553
+ createdAt: 1700000000000,
554
+ updatedAt: 1700000000000,
555
+ },
556
+ ],
557
+ };
558
+ writeFileSync(
559
+ join(TEST_DIR, "metadata.json"),
560
+ JSON.stringify(v2Data, null, 2),
561
+ "utf-8",
562
+ );
563
+
564
+ const meta = getCredentialMetadata("integration:gmail", "access_token");
565
+ expect(meta).toBeDefined();
566
+ expect("oauth2ClientSecret" in meta!).toBe(false);
567
+
568
+ // Verify on-disk file no longer contains the secret
569
+ const raw = JSON.parse(
570
+ readFileSync(join(TEST_DIR, "metadata.json"), "utf-8"),
571
+ );
572
+ expect(raw.credentials[0]).not.toHaveProperty("oauth2ClientSecret");
573
+ expect(raw.version).toBe(5);
574
+ });
575
+ });
576
+
473
577
  // ---------------------------------------------------------------------------
474
578
  // Cross-Cutting — One-Time Send Override
475
579
  // ---------------------------------------------------------------------------