@vellumai/assistant 0.4.48 → 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 (252) hide show
  1. package/ARCHITECTURE.md +2 -2
  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/runbook-trusted-contacts.md +3 -8
  6. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  7. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +0 -1
  10. package/src/__tests__/anthropic-provider.test.ts +156 -0
  11. package/src/__tests__/approval-cascade.test.ts +810 -0
  12. package/src/__tests__/approval-primitive.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +2 -0
  14. package/src/__tests__/assistant-attachments.test.ts +12 -34
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  17. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  18. package/src/__tests__/channel-guardian.test.ts +0 -2
  19. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  20. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  21. package/src/__tests__/checker.test.ts +9 -29
  22. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  23. package/src/__tests__/computer-use-tools.test.ts +2 -19
  24. package/src/__tests__/config-watcher.test.ts +0 -1
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  26. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  27. package/src/__tests__/context-token-estimator.test.ts +196 -13
  28. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  29. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  30. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  31. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  32. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  33. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  34. package/src/__tests__/credential-vault-unit.test.ts +280 -49
  35. package/src/__tests__/credential-vault.test.ts +138 -16
  36. package/src/__tests__/credentials-cli.test.ts +71 -0
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  38. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  39. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  40. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  41. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  42. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  43. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  44. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  45. package/src/__tests__/heartbeat-service.test.ts +0 -1
  46. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  47. package/src/__tests__/host-shell-tool.test.ts +27 -15
  48. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  49. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  50. package/src/__tests__/integration-status.test.ts +32 -51
  51. package/src/__tests__/intent-routing.test.ts +0 -1
  52. package/src/__tests__/invite-routes-http.test.ts +10 -9
  53. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  54. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  55. package/src/__tests__/oauth-cli.test.ts +373 -14
  56. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  57. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  58. package/src/__tests__/oauth-store.test.ts +756 -0
  59. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  60. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  61. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  62. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  63. package/src/__tests__/recording-handler.test.ts +3 -4
  64. package/src/__tests__/registry.test.ts +2 -2
  65. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  66. package/src/__tests__/schedule-store.test.ts +0 -1
  67. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  68. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  69. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  70. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  71. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  72. package/src/__tests__/sequence-store.test.ts +0 -1
  73. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  74. package/src/__tests__/skill-include-graph.test.ts +66 -0
  75. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  76. package/src/__tests__/skill-load-tool.test.ts +149 -1
  77. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  78. package/src/__tests__/skills-uninstall.test.ts +1 -1
  79. package/src/__tests__/skills.test.ts +3 -3
  80. package/src/__tests__/slack-channel-config.test.ts +67 -3
  81. package/src/__tests__/slack-share-routes.test.ts +17 -19
  82. package/src/__tests__/system-prompt.test.ts +0 -1
  83. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  84. package/src/__tests__/terminal-tools.test.ts +4 -3
  85. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  86. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  87. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  92. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  93. package/src/__tests__/trust-store.test.ts +1 -22
  94. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  95. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  96. package/src/__tests__/twilio-routes.test.ts +0 -16
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  99. package/src/agent/ax-tree-compaction.test.ts +235 -0
  100. package/src/agent/loop.ts +76 -130
  101. package/src/calls/call-domain.ts +1 -6
  102. package/src/calls/relay-server.ts +9 -13
  103. package/src/calls/twilio-config.ts +2 -7
  104. package/src/calls/twilio-routes.ts +1 -2
  105. package/src/calls/voice-ingress-preflight.ts +1 -1
  106. package/src/cli/commands/browser-relay.ts +18 -12
  107. package/src/cli/commands/completions.ts +0 -3
  108. package/src/cli/commands/credentials.ts +101 -15
  109. package/src/cli/commands/oauth/apps.ts +255 -0
  110. package/src/cli/commands/oauth/connections.ts +299 -0
  111. package/src/cli/commands/oauth/index.ts +52 -0
  112. package/src/cli/commands/oauth/providers.ts +242 -0
  113. package/src/cli/commands/skills.ts +4 -338
  114. package/src/cli/program.ts +1 -5
  115. package/src/cli/reference.ts +1 -3
  116. package/src/config/assistant-feature-flags.ts +0 -3
  117. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  118. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  119. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  120. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  121. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  122. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  123. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  124. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  125. package/src/config/env-registry.ts +14 -83
  126. package/src/config/env.ts +11 -50
  127. package/src/config/feature-flag-registry.json +16 -16
  128. package/src/config/loader.ts +0 -6
  129. package/src/config/schema.ts +3 -1
  130. package/src/config/skills.ts +21 -2
  131. package/src/context/image-dimensions.ts +229 -0
  132. package/src/context/token-estimator.ts +75 -12
  133. package/src/context/window-manager.ts +49 -10
  134. package/src/daemon/assistant-attachments.ts +1 -13
  135. package/src/daemon/handlers/config-ingress.ts +8 -33
  136. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  137. package/src/daemon/handlers/config-telegram.ts +32 -16
  138. package/src/daemon/handlers/sessions.ts +10 -24
  139. package/src/daemon/handlers/shared.ts +0 -130
  140. package/src/daemon/host-cu-proxy.ts +401 -0
  141. package/src/daemon/lifecycle.ts +36 -68
  142. package/src/daemon/message-protocol.ts +3 -0
  143. package/src/daemon/message-types/computer-use.ts +2 -119
  144. package/src/daemon/message-types/host-cu.ts +19 -0
  145. package/src/daemon/message-types/messages.ts +3 -0
  146. package/src/daemon/server.ts +14 -21
  147. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  148. package/src/daemon/session-attachments.ts +1 -2
  149. package/src/daemon/session-slash.ts +1 -1
  150. package/src/daemon/session-surfaces.ts +40 -28
  151. package/src/daemon/session-tool-setup.ts +2 -9
  152. package/src/daemon/session.ts +138 -15
  153. package/src/daemon/tool-side-effects.ts +2 -8
  154. package/src/daemon/watch-handler.ts +2 -2
  155. package/src/events/tool-metrics-listener.ts +2 -2
  156. package/src/hooks/manager.ts +1 -4
  157. package/src/inbound/public-ingress-urls.ts +7 -7
  158. package/src/logfire.ts +16 -5
  159. package/src/memory/conversation-key-store.ts +21 -0
  160. package/src/memory/db-init.ts +4 -0
  161. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  162. package/src/memory/migrations/index.ts +1 -0
  163. package/src/memory/schema/index.ts +1 -0
  164. package/src/memory/schema/oauth.ts +65 -0
  165. package/src/messaging/provider.ts +4 -4
  166. package/src/messaging/providers/gmail/client.ts +82 -2
  167. package/src/messaging/providers/gmail/people-client.ts +10 -10
  168. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  169. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  170. package/src/messaging/registry.ts +2 -32
  171. package/src/notifications/copy-composer.ts +0 -5
  172. package/src/notifications/signal.ts +4 -5
  173. package/src/oauth/byo-connection.test.ts +126 -25
  174. package/src/oauth/byo-connection.ts +22 -6
  175. package/src/oauth/connect-orchestrator.ts +113 -57
  176. package/src/oauth/connect-types.ts +17 -23
  177. package/src/oauth/connection-resolver.ts +35 -11
  178. package/src/oauth/connection.ts +1 -1
  179. package/src/oauth/manual-token-connection.ts +104 -0
  180. package/src/oauth/oauth-store.ts +496 -0
  181. package/src/oauth/platform-connection.test.ts +29 -0
  182. package/src/oauth/platform-connection.ts +6 -5
  183. package/src/oauth/provider-behaviors.ts +124 -0
  184. package/src/oauth/scope-policy.ts +9 -2
  185. package/src/oauth/seed-providers.ts +161 -0
  186. package/src/oauth/token-persistence.ts +74 -78
  187. package/src/permissions/checker.ts +3 -3
  188. package/src/permissions/defaults.ts +0 -1
  189. package/src/permissions/prompter.ts +10 -1
  190. package/src/permissions/trust-store.ts +13 -0
  191. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  192. package/src/prompts/system-prompt.ts +28 -40
  193. package/src/providers/anthropic/client.ts +133 -24
  194. package/src/providers/retry.ts +1 -27
  195. package/src/runtime/auth/route-policy.ts +0 -3
  196. package/src/runtime/channel-reply-delivery.ts +0 -40
  197. package/src/runtime/gateway-client.ts +0 -7
  198. package/src/runtime/http-server.ts +8 -6
  199. package/src/runtime/http-types.ts +2 -2
  200. package/src/runtime/middleware/twilio-validation.ts +1 -11
  201. package/src/runtime/pending-interactions.ts +14 -12
  202. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  203. package/src/runtime/routes/conversation-routes.ts +73 -19
  204. package/src/runtime/routes/events-routes.ts +21 -11
  205. package/src/runtime/routes/host-cu-routes.ts +97 -0
  206. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  207. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  208. package/src/runtime/routes/log-export-routes.ts +126 -8
  209. package/src/runtime/routes/settings-routes.ts +55 -48
  210. package/src/runtime/routes/surface-action-routes.ts +1 -1
  211. package/src/runtime/routes/watch-routes.ts +128 -0
  212. package/src/schedule/integration-status.ts +10 -9
  213. package/src/security/credential-key.ts +0 -156
  214. package/src/security/keychain-broker-client.ts +5 -6
  215. package/src/security/oauth2.ts +1 -1
  216. package/src/security/token-manager.ts +119 -46
  217. package/src/skills/catalog-install.ts +358 -0
  218. package/src/skills/include-graph.ts +32 -0
  219. package/src/telegram/bot-username.ts +2 -3
  220. package/src/tools/browser/network-recorder.ts +1 -1
  221. package/src/tools/browser/network-recording-types.ts +1 -1
  222. package/src/tools/computer-use/definitions.ts +46 -11
  223. package/src/tools/computer-use/registry.ts +4 -5
  224. package/src/tools/credentials/broker.ts +1 -2
  225. package/src/tools/credentials/metadata-store.ts +17 -121
  226. package/src/tools/credentials/vault.ts +94 -167
  227. package/src/tools/registry.ts +2 -7
  228. package/src/tools/skills/load.ts +62 -3
  229. package/src/tools/watch/watch-state.ts +0 -12
  230. package/src/util/logger.ts +7 -41
  231. package/src/util/platform.ts +9 -28
  232. package/src/watcher/providers/google-calendar.ts +2 -1
  233. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  234. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  235. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  236. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  237. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  238. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  239. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  240. package/src/cli/commands/dev.ts +0 -129
  241. package/src/cli/commands/map.ts +0 -391
  242. package/src/cli/commands/oauth.ts +0 -77
  243. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  244. package/src/daemon/computer-use-session.ts +0 -1026
  245. package/src/daemon/ride-shotgun-handler.ts +0 -569
  246. package/src/oauth/provider-base-urls.ts +0 -21
  247. package/src/oauth/provider-profiles.ts +0 -192
  248. package/src/prompts/computer-use-prompt.ts +0 -98
  249. package/src/runtime/routes/computer-use-routes.ts +0 -641
  250. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  251. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  252. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -171,6 +171,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
171
171
  hasPendingConfirmation: () => false,
172
172
  setHostBashProxy: () => {},
173
173
  setHostFileProxy: () => {},
174
+ setHostCuProxy: () => {},
174
175
  } as unknown as import("../daemon/session.js").Session;
175
176
 
176
177
  const req = new Request("http://localhost/v1/messages", {
@@ -246,6 +247,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
246
247
  hasPendingConfirmation: () => false,
247
248
  setHostBashProxy: () => {},
248
249
  setHostFileProxy: () => {},
250
+ setHostCuProxy: () => {},
249
251
  } as unknown as import("../daemon/session.js").Session;
250
252
 
251
253
  const req = new Request("http://localhost/v1/messages", {
@@ -317,6 +319,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
317
319
  requestId === "tool-approval-live",
318
320
  setHostBashProxy: () => {},
319
321
  setHostFileProxy: () => {},
322
+ setHostCuProxy: () => {},
320
323
  } as unknown as import("../daemon/session.js").Session;
321
324
 
322
325
  const req = new Request("http://localhost/v1/messages", {
@@ -392,6 +395,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
392
395
  hasPendingConfirmation: (id: string) => id === "tool-req-code-1",
393
396
  setHostBashProxy: () => {},
394
397
  setHostFileProxy: () => {},
398
+ setHostCuProxy: () => {},
395
399
  } as unknown as import("../daemon/session.js").Session;
396
400
 
397
401
  const req = new Request("http://localhost/v1/messages", {
@@ -463,6 +467,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
463
467
  hasPendingConfirmation: (id: string) => id === "pending-reject-1",
464
468
  setHostBashProxy: () => {},
465
469
  setHostFileProxy: () => {},
470
+ setHostCuProxy: () => {},
466
471
  } as unknown as import("../daemon/session.js").Session;
467
472
 
468
473
  const req = new Request("http://localhost/v1/messages", {
@@ -528,6 +533,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
528
533
  hasPendingConfirmation: (id: string) => id === "pending-1",
529
534
  setHostBashProxy: () => {},
530
535
  setHostFileProxy: () => {},
536
+ setHostCuProxy: () => {},
531
537
  } as unknown as import("../daemon/session.js").Session;
532
538
 
533
539
  const req = new Request("http://localhost/v1/messages", {
@@ -559,4 +565,142 @@ describe("handleSendMessage canonical guardian reply interception", () => {
559
565
  expect(persistUserMessage).toHaveBeenCalledTimes(1);
560
566
  expect(runAgentLoop).toHaveBeenCalledTimes(1);
561
567
  });
568
+
569
+ test("desktop sessions do not pass approvalConversationGenerator to routeGuardianReply", async () => {
570
+ listPendingByDestinationMock.mockReturnValue([
571
+ { id: "pending-1", kind: "access_request" },
572
+ ]);
573
+ listCanonicalMock.mockReturnValue([]);
574
+ routeGuardianReplyMock.mockResolvedValue({
575
+ consumed: false,
576
+ decisionApplied: false,
577
+ type: "not_consumed",
578
+ });
579
+
580
+ const mockGenerator = mock(async () => ({}));
581
+ const persistUserMessage = mock(async () => "persisted-user-id");
582
+ const runAgentLoop = mock(async () => undefined);
583
+ const session = {
584
+ setTrustContext: () => {},
585
+ updateClient: () => {},
586
+ emitConfirmationStateChanged: () => {},
587
+ emitActivityState: () => {},
588
+ setTurnChannelContext: () => {},
589
+ setTurnInterfaceContext: () => {},
590
+ ensureActorScopedHistory: async () => {},
591
+ usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
592
+ isProcessing: () => false,
593
+ hasAnyPendingConfirmation: () => false,
594
+ denyAllPendingConfirmations: () => {},
595
+ enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
596
+ persistUserMessage,
597
+ runAgentLoop,
598
+ getMessages: () => [] as unknown[],
599
+ assistantId: "self",
600
+ trustContext: undefined,
601
+ hasPendingConfirmation: () => false,
602
+ setHostBashProxy: () => {},
603
+ setHostFileProxy: () => {},
604
+ setHostCuProxy: () => {},
605
+ } as unknown as import("../daemon/session.js").Session;
606
+
607
+ const req = new Request("http://localhost/v1/messages", {
608
+ method: "POST",
609
+ headers: { "Content-Type": "application/json" },
610
+ body: JSON.stringify({
611
+ conversationKey: "guardian-thread-key",
612
+ content: "no sorry, beats 0 and 3 should be new threads",
613
+ sourceChannel: "vellum",
614
+ interface: "macos",
615
+ }),
616
+ });
617
+
618
+ await handleSendMessage(
619
+ req,
620
+ {
621
+ sendMessageDeps: {
622
+ getOrCreateSession: async () => session,
623
+ assistantEventHub: { publish: async () => {} } as any,
624
+ resolveAttachments: () => [],
625
+ },
626
+ approvalConversationGenerator: mockGenerator as any,
627
+ },
628
+ testAuthContext,
629
+ );
630
+
631
+ expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
632
+ const routerCall = (routeGuardianReplyMock as any).mock
633
+ .calls[0][0] as Record<string, unknown>;
634
+ // Desktop (vellum) should suppress the NL engine
635
+ expect(routerCall.approvalConversationGenerator).toBeUndefined();
636
+ });
637
+
638
+ test("channel sessions pass approvalConversationGenerator to routeGuardianReply", async () => {
639
+ listPendingByDestinationMock.mockReturnValue([
640
+ { id: "pending-1", kind: "access_request" },
641
+ ]);
642
+ listCanonicalMock.mockReturnValue([]);
643
+ routeGuardianReplyMock.mockResolvedValue({
644
+ consumed: false,
645
+ decisionApplied: false,
646
+ type: "not_consumed",
647
+ });
648
+
649
+ const mockGenerator = mock(async () => ({}));
650
+ const persistUserMessage = mock(async () => "persisted-user-id");
651
+ const runAgentLoop = mock(async () => undefined);
652
+ const session = {
653
+ setTrustContext: () => {},
654
+ updateClient: () => {},
655
+ emitConfirmationStateChanged: () => {},
656
+ emitActivityState: () => {},
657
+ setTurnChannelContext: () => {},
658
+ setTurnInterfaceContext: () => {},
659
+ ensureActorScopedHistory: async () => {},
660
+ usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
661
+ isProcessing: () => false,
662
+ hasAnyPendingConfirmation: () => false,
663
+ denyAllPendingConfirmations: () => {},
664
+ enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
665
+ persistUserMessage,
666
+ runAgentLoop,
667
+ getMessages: () => [] as unknown[],
668
+ assistantId: "self",
669
+ trustContext: undefined,
670
+ hasPendingConfirmation: () => false,
671
+ setHostBashProxy: () => {},
672
+ setHostFileProxy: () => {},
673
+ setHostCuProxy: () => {},
674
+ } as unknown as import("../daemon/session.js").Session;
675
+
676
+ const req = new Request("http://localhost/v1/messages", {
677
+ method: "POST",
678
+ headers: { "Content-Type": "application/json" },
679
+ body: JSON.stringify({
680
+ conversationKey: "guardian-thread-key",
681
+ content: "no sorry, beats 0 and 3 should be new threads",
682
+ sourceChannel: "telegram",
683
+ interface: "telegram",
684
+ }),
685
+ });
686
+
687
+ await handleSendMessage(
688
+ req,
689
+ {
690
+ sendMessageDeps: {
691
+ getOrCreateSession: async () => session,
692
+ assistantEventHub: { publish: async () => {} } as any,
693
+ resolveAttachments: () => [],
694
+ },
695
+ approvalConversationGenerator: mockGenerator as any,
696
+ },
697
+ testAuthContext,
698
+ );
699
+
700
+ expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
701
+ const routerCall = (routeGuardianReplyMock as any).mock
702
+ .calls[0][0] as Record<string, unknown>;
703
+ // Channel sessions should receive the NL engine
704
+ expect(routerCall.approvalConversationGenerator).toBe(mockGenerator);
705
+ });
562
706
  });
@@ -203,6 +203,7 @@ function makeSession() {
203
203
  hasPendingConfirmation: () => false,
204
204
  setHostBashProxy: () => {},
205
205
  setHostFileProxy: () => {},
206
+ setHostCuProxy: () => {},
206
207
  usageStats: {
207
208
  inputTokens: 1000,
208
209
  outputTokens: 500,
@@ -294,49 +294,35 @@ describe("credential metadata store", () => {
294
294
  });
295
295
  });
296
296
 
297
- // ── v4 Schema: hasRefreshToken ──────────────────────────────────────
297
+ // ── v5 Schema: OAuth fields removed ─────────────────────────────────
298
298
 
299
- describe("v4 schema — hasRefreshToken", () => {
300
- test("creates record with hasRefreshToken", () => {
299
+ describe("v5 schema — OAuth fields removed from CredentialMetadata", () => {
300
+ test("CredentialMetadata does not include hasRefreshToken", () => {
301
301
  const record = upsertCredentialMetadata("github", "access_token", {
302
- hasRefreshToken: true,
302
+ allowedTools: ["api_request"],
303
303
  });
304
- expect(record.hasRefreshToken).toBe(true);
304
+ // hasRefreshToken was removed in v5 — field should not exist
305
+ expect("hasRefreshToken" in record).toBe(false);
305
306
  });
306
307
 
307
- test("creates record with hasRefreshToken false", () => {
308
- const record = upsertCredentialMetadata("github", "access_token", {
309
- hasRefreshToken: false,
310
- });
311
- expect(record.hasRefreshToken).toBe(false);
308
+ test("CredentialMetadata does not include oauth2TokenUrl", () => {
309
+ const record = upsertCredentialMetadata("github", "access_token");
310
+ expect("oauth2TokenUrl" in record).toBe(false);
312
311
  });
313
312
 
314
- test("defaults hasRefreshToken to undefined when not provided", () => {
313
+ test("CredentialMetadata does not include oauth2ClientId", () => {
315
314
  const record = upsertCredentialMetadata("github", "access_token");
316
- expect(record.hasRefreshToken).toBeUndefined();
315
+ expect("oauth2ClientId" in record).toBe(false);
317
316
  });
318
317
 
319
- test("updates hasRefreshToken on existing record", () => {
320
- upsertCredentialMetadata("github", "access_token", {
321
- hasRefreshToken: false,
322
- });
323
- const updated = upsertCredentialMetadata("github", "access_token", {
324
- hasRefreshToken: true,
325
- });
326
- expect(updated.hasRefreshToken).toBe(true);
318
+ test("CredentialMetadata does not include expiresAt", () => {
319
+ const record = upsertCredentialMetadata("github", "access_token");
320
+ expect("expiresAt" in record).toBe(false);
327
321
  });
328
322
 
329
- test("round-trip: hasRefreshToken survives serialization", () => {
330
- upsertCredentialMetadata("github", "access_token", {
331
- hasRefreshToken: true,
332
- allowedTools: ["api_request"],
333
- });
334
-
335
- // Re-read from disk
336
- const loaded = getCredentialMetadata("github", "access_token");
337
- expect(loaded).toBeDefined();
338
- expect(loaded!.hasRefreshToken).toBe(true);
339
- expect(loaded!.allowedTools).toEqual(["api_request"]);
323
+ test("CredentialMetadata does not include grantedScopes", () => {
324
+ const record = upsertCredentialMetadata("github", "access_token");
325
+ expect("grantedScopes" in record).toBe(false);
340
326
  });
341
327
  });
342
328
 
@@ -417,7 +403,7 @@ describe("credential metadata store", () => {
417
403
  expect(record!.credentialId).toBe("cred-stable-id");
418
404
  });
419
405
 
420
- test("v2 file is migrated to v4 (strips oauth2ClientSecret)", () => {
406
+ test("v2 file is migrated to v5 (strips oauth2ClientSecret and OAuth fields)", () => {
421
407
  const v2Data = {
422
408
  version: 2,
423
409
  credentials: [
@@ -449,16 +435,16 @@ describe("credential metadata store", () => {
449
435
  expect(record!.alias).toBe("fal-primary");
450
436
  expect(record!.injectionTemplates).toHaveLength(1);
451
437
  expect(record!.injectionTemplates![0].hostPattern).toBe("*.fal.ai");
452
- // oauth2ClientSecret must be stripped by v2→v3 migration
438
+ // oauth2ClientSecret must be stripped by migration
453
439
  expect("oauth2ClientSecret" in record!).toBe(false);
454
440
 
455
- // On-disk file should be upgraded to v4
441
+ // On-disk file should be upgraded to v5
456
442
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
457
- expect(raw.version).toBe(4);
443
+ expect(raw.version).toBe(5);
458
444
  expect(raw.credentials[0]).not.toHaveProperty("oauth2ClientSecret");
459
445
  });
460
446
 
461
- test("v3 file is migrated to v4 (removes ghost refresh_token records)", () => {
447
+ test("v3 file is migrated to v5 (removes ghost refresh_token records)", () => {
462
448
  const v3Data = {
463
449
  version: 3,
464
450
  credentials: [
@@ -488,11 +474,12 @@ describe("credential metadata store", () => {
488
474
  // Ghost refresh_token record removed
489
475
  expect(records).toHaveLength(1);
490
476
  expect(records[0].field).toBe("access_token");
491
- expect(records[0].hasRefreshToken).toBe(true);
477
+ // hasRefreshToken is stripped in v5 migration (OAuth fields moved to SQLite)
478
+ expect("hasRefreshToken" in records[0]).toBe(false);
492
479
 
493
- // On-disk file should be upgraded to v4
480
+ // On-disk file should be upgraded to v5
494
481
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
495
- expect(raw.version).toBe(4);
482
+ expect(raw.version).toBe(5);
496
483
  expect(raw.credentials).toHaveLength(1);
497
484
  expect(raw.credentials[0].field).toBe("access_token");
498
485
  });
@@ -528,11 +515,12 @@ describe("credential metadata store", () => {
528
515
  expect(record!.alias).toBe("fal-primary");
529
516
  expect(record!.injectionTemplates).toHaveLength(1);
530
517
  expect(record!.injectionTemplates![0].hostPattern).toBe("*.fal.ai");
531
- expect(record!.hasRefreshToken).toBeUndefined();
518
+ // hasRefreshToken is stripped in v5 migration
519
+ expect("hasRefreshToken" in record!).toBe(false);
532
520
 
533
- // On-disk file should be upgraded to v4
521
+ // On-disk file should be upgraded to v5
534
522
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
535
- expect(raw.version).toBe(4);
523
+ expect(raw.version).toBe(5);
536
524
  });
537
525
 
538
526
  test("v3 migration handles multiple services with ghost records", () => {
@@ -592,16 +580,16 @@ describe("credential metadata store", () => {
592
580
  // refresh_token records removed, only access_token records remain
593
581
  expect(records).toHaveLength(3);
594
582
  expect(records.every((r) => r.field !== "refresh_token")).toBe(true);
595
- // github and stripe had refresh tokens
583
+ // hasRefreshToken is stripped in v5 migration — none should have it
596
584
  const github = records.find((r) => r.service === "github");
597
585
  const slack = records.find((r) => r.service === "slack");
598
586
  const stripe = records.find((r) => r.service === "stripe");
599
- expect(github!.hasRefreshToken).toBe(true);
600
- expect(slack!.hasRefreshToken).toBeUndefined();
601
- expect(stripe!.hasRefreshToken).toBe(true);
587
+ expect("hasRefreshToken" in github!).toBe(false);
588
+ expect("hasRefreshToken" in slack!).toBe(false);
589
+ expect("hasRefreshToken" in stripe!).toBe(false);
602
590
  });
603
591
 
604
- test("v4 file is loaded without migration", () => {
592
+ test("v4 file is migrated to v5 (strips hasRefreshToken and OAuth fields)", () => {
605
593
  const v4Data = {
606
594
  version: 4,
607
595
  credentials: [
@@ -623,10 +611,15 @@ describe("credential metadata store", () => {
623
611
  const record = getCredentialMetadata("fal-ai", "api_key");
624
612
  expect(record).toBeDefined();
625
613
  expect(record!.alias).toBe("fal-primary");
626
- expect(record!.hasRefreshToken).toBe(true);
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);
627
620
  });
628
621
 
629
- test("future version (v5+) returns unknown version and refuses writes", () => {
622
+ test("future version (v6+) returns unknown version and refuses writes", () => {
630
623
  const futureData = {
631
624
  version: 99,
632
625
  credentials: [],
@@ -661,7 +654,7 @@ describe("credential metadata store", () => {
661
654
  },
662
655
  );
663
656
 
664
- test("upsert on migrated v1 file saves as v4", () => {
657
+ test("upsert on migrated v1 file saves as v5", () => {
665
658
  const v1Data = {
666
659
  version: 1,
667
660
  credentials: [
@@ -681,13 +674,13 @@ describe("credential metadata store", () => {
681
674
  // Upsert triggers load (migration) + save (at current version)
682
675
  upsertCredentialMetadata("github", "token", { alias: "gh-main" });
683
676
 
684
- // Verify on-disk file is now v4
677
+ // Verify on-disk file is now v5
685
678
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
686
- expect(raw.version).toBe(4);
679
+ expect(raw.version).toBe(5);
687
680
  expect(raw.credentials[0].alias).toBe("gh-main");
688
681
  });
689
682
 
690
- test("v1 load auto-persists as v4 on disk without requiring a write", () => {
683
+ test("v1 load auto-persists as v5 on disk without requiring a write", () => {
691
684
  const v1Data = {
692
685
  version: 1,
693
686
  credentials: [
@@ -704,15 +697,15 @@ describe("credential metadata store", () => {
704
697
  };
705
698
  writeFileSync(META_PATH, JSON.stringify(v1Data, null, 2), "utf-8");
706
699
 
707
- // A read-only operation should still persist the v4 upgrade
700
+ // A read-only operation should still persist the v5 upgrade
708
701
  listCredentialMetadata();
709
702
 
710
703
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
711
- expect(raw.version).toBe(4);
704
+ expect(raw.version).toBe(5);
712
705
  expect(raw.credentials[0].credentialId).toBe("cred-autopersist");
713
706
  });
714
707
 
715
- test("v1 file with multiple credentials migrates all records", () => {
708
+ test("v1 file with multiple credentials migrates all records (strips OAuth fields)", () => {
716
709
  const v1Data = {
717
710
  version: 1,
718
711
  credentials: [
@@ -761,13 +754,11 @@ describe("credential metadata store", () => {
761
754
  expect(r.injectionTemplates).toBeUndefined();
762
755
  }
763
756
 
764
- // Original v1 fields preserved
765
- expect(records[0].grantedScopes).toEqual(["repo", "user"]);
766
- expect(records[1].expiresAt).toBe(1800000000000);
767
- expect(records[2].oauth2TokenUrl).toBe(
768
- "https://connect.stripe.com/oauth/token",
769
- );
770
- 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);
771
762
  });
772
763
  });
773
764
 
@@ -808,20 +799,20 @@ describe("credential metadata store", () => {
808
799
  test("file with non-array credentials field is treated as empty list", () => {
809
800
  writeFileSync(
810
801
  META_PATH,
811
- JSON.stringify({ version: 4, credentials: "not-an-array" }),
802
+ JSON.stringify({ version: 5, credentials: "not-an-array" }),
812
803
  "utf-8",
813
804
  );
814
805
  expect(listCredentialMetadata()).toEqual([]);
815
806
  });
816
807
 
817
808
  test("file with missing credentials field is treated as empty list", () => {
818
- writeFileSync(META_PATH, JSON.stringify({ version: 4 }), "utf-8");
809
+ writeFileSync(META_PATH, JSON.stringify({ version: 5 }), "utf-8");
819
810
  expect(listCredentialMetadata()).toEqual([]);
820
811
  });
821
812
 
822
813
  test("malformed records within credentials array are filtered out", () => {
823
814
  const data = {
824
- version: 4,
815
+ version: 5,
825
816
  credentials: [
826
817
  // Valid record
827
818
  {
@@ -931,7 +922,7 @@ describe("credential metadata store", () => {
931
922
 
932
923
  const raw = readFileSync(META_PATH, "utf-8");
933
924
  const parsed = JSON.parse(raw);
934
- expect(parsed.version).toBe(4);
925
+ expect(parsed.version).toBe(5);
935
926
  expect(parsed.credentials).toHaveLength(1);
936
927
  expect(parsed.credentials[0].service).toBe("slack");
937
928
  });
@@ -939,23 +930,23 @@ describe("credential metadata store", () => {
939
930
  test("file written by saveFile has version field", () => {
940
931
  upsertCredentialMetadata("test", "key");
941
932
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
942
- expect(raw.version).toBe(4);
933
+ expect(raw.version).toBe(5);
943
934
  });
944
935
  });
945
936
 
946
937
  // ── Empty credential lists ────────────────────────────────────────
947
938
 
948
939
  describe("empty credential lists", () => {
949
- test("empty v4 file returns empty array", () => {
940
+ test("empty v5 file returns empty array", () => {
950
941
  writeFileSync(
951
942
  META_PATH,
952
- JSON.stringify({ version: 4, credentials: [] }, null, 2),
943
+ JSON.stringify({ version: 5, credentials: [] }, null, 2),
953
944
  "utf-8",
954
945
  );
955
946
  expect(listCredentialMetadata()).toEqual([]);
956
947
  });
957
948
 
958
- test("empty v1 file is migrated to v4 with empty credentials", () => {
949
+ test("empty v1 file is migrated to v5 with empty credentials", () => {
959
950
  writeFileSync(
960
951
  META_PATH,
961
952
  JSON.stringify({ version: 1, credentials: [] }, null, 2),
@@ -963,9 +954,9 @@ describe("credential metadata store", () => {
963
954
  );
964
955
  expect(listCredentialMetadata()).toEqual([]);
965
956
 
966
- // Should be persisted as v4
957
+ // Should be persisted as v5
967
958
  const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
968
- expect(raw.version).toBe(4);
959
+ expect(raw.version).toBe(5);
969
960
  expect(raw.credentials).toEqual([]);
970
961
  });
971
962
 
@@ -229,10 +229,13 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
229
229
  "runtime/routes/integrations/slack/share.ts", // Slack share routes credential lookup
230
230
  "mcp/client.ts", // MCP client cached-token lookup
231
231
  "oauth/token-persistence.ts", // OAuth token persistence (set/delete tokens)
232
+ "oauth/connection-resolver.ts", // resolve OAuthConnection from oauth-store (access_token lookup)
232
233
  "runtime/routes/secret-routes.ts", // HTTP secret management routes (set/delete secrets)
233
- "daemon/ride-shotgun-handler.ts", // learn session cookie persistence
234
234
  "daemon/session-messaging.ts", // credential storage during session messaging
235
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)
236
239
  ]);
237
240
 
238
241
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -499,16 +502,17 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
499
502
  rmSync(TEST_DIR, { recursive: true, force: true });
500
503
  });
501
504
 
502
- test("upsertCredentialMetadata does not accept oauth2ClientSecret", () => {
505
+ test("upsertCredentialMetadata does not accept oauth2ClientSecret or other OAuth fields", () => {
503
506
  const record = upsertCredentialMetadata(
504
507
  "integration:gmail",
505
508
  "access_token",
506
509
  {
507
- oauth2TokenUrl: "https://oauth2.googleapis.com/token",
508
- oauth2ClientId: "test-client-id",
510
+ allowedTools: ["api_request"],
509
511
  },
510
512
  );
511
513
  expect("oauth2ClientSecret" in record).toBe(false);
514
+ expect("oauth2TokenUrl" in record).toBe(false);
515
+ expect("oauth2ClientId" in record).toBe(false);
512
516
  });
513
517
 
514
518
  test("client secret is read from secure store, not metadata", () => {
@@ -517,13 +521,15 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
517
521
  "my-secret",
518
522
  );
519
523
  upsertCredentialMetadata("integration:gmail", "access_token", {
520
- oauth2TokenUrl: "https://oauth2.googleapis.com/token",
521
- oauth2ClientId: "test-client-id",
524
+ allowedTools: ["api_request"],
522
525
  });
523
526
 
524
527
  const meta = getCredentialMetadata("integration:gmail", "access_token");
525
528
  expect(meta).toBeDefined();
526
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);
527
533
 
528
534
  // Secret is in secure store
529
535
  expect(
@@ -564,7 +570,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
564
570
  readFileSync(join(TEST_DIR, "metadata.json"), "utf-8"),
565
571
  );
566
572
  expect(raw.credentials[0]).not.toHaveProperty("oauth2ClientSecret");
567
- expect(raw.version).toBe(4);
573
+ expect(raw.version).toBe(5);
568
574
  });
569
575
  });
570
576