@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
@@ -2,6 +2,8 @@ import { 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";
6
+
5
7
  // ---------------------------------------------------------------------------
6
8
  // Mock state
7
9
  // ---------------------------------------------------------------------------
@@ -11,6 +13,28 @@ let mockWithValidToken: <T>(
11
13
  cb: (token: string) => Promise<T>,
12
14
  ) => Promise<T>;
13
15
 
16
+ // Disconnect mock state
17
+ let mockListProviders: () => Array<Record<string, unknown>> = () => [];
18
+ let secureKeyStore = new Map<string, string>();
19
+ let metadataStore: Array<{
20
+ credentialId: string;
21
+ service: string;
22
+ field: string;
23
+ allowedTools: string[];
24
+ allowedDomains: string[];
25
+ createdAt: number;
26
+ updatedAt: number;
27
+ }> = [];
28
+ let disconnectOAuthProviderCalls: string[] = [];
29
+ let disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
30
+ "not-found";
31
+ let idCounter = 0;
32
+
33
+ function nextUUID(): string {
34
+ idCounter += 1;
35
+ return `00000000-0000-0000-0000-${String(idCounter).padStart(12, "0")}`;
36
+ }
37
+
14
38
  // ---------------------------------------------------------------------------
15
39
  // Mock token-manager
16
40
  // ---------------------------------------------------------------------------
@@ -32,19 +56,77 @@ mock.module("../security/token-manager.js", () => ({
32
56
  },
33
57
  }));
34
58
 
59
+ // ---------------------------------------------------------------------------
60
+ // Mock oauth-store (stateful for disconnect tests)
61
+ // ---------------------------------------------------------------------------
62
+
63
+ mock.module("../oauth/oauth-store.js", () => ({
64
+ disconnectOAuthProvider: async (
65
+ providerKey: string,
66
+ ): Promise<"disconnected" | "not-found" | "error"> => {
67
+ disconnectOAuthProviderCalls.push(providerKey);
68
+ return disconnectOAuthProviderResult;
69
+ },
70
+ getConnection: () => undefined,
71
+ getConnectionByProvider: () => undefined,
72
+ listConnections: () => [],
73
+ deleteConnection: () => false,
74
+ // Stubs required by apps.ts and providers.ts (transitively loaded via oauth/index.ts)
75
+ upsertApp: async () => ({}),
76
+ getApp: () => undefined,
77
+ getAppByProviderAndClientId: () => undefined,
78
+ getMostRecentAppByProvider: () => undefined,
79
+ listApps: () => [],
80
+ deleteApp: async () => false,
81
+ getProvider: () => undefined,
82
+ listProviders: () => mockListProviders(),
83
+ registerProvider: () => ({}),
84
+ seedProviders: () => {},
85
+ createConnection: () => ({}),
86
+ isProviderConnected: () => false,
87
+ updateConnection: () => ({}),
88
+ }));
89
+
35
90
  // Stub out transitive dependencies that token-manager would normally pull in
36
91
  mock.module("../security/secure-keys.js", () => ({
37
92
  getSecureKey: () => undefined,
38
93
  setSecureKey: () => true,
39
94
  getSecureKeyAsync: async () => undefined,
40
95
  setSecureKeyAsync: async () => true,
41
- deleteSecureKey: () => "not-found",
96
+ deleteSecureKey: (account: string) => {
97
+ if (secureKeyStore.has(account)) {
98
+ secureKeyStore.delete(account);
99
+ return "deleted" as const;
100
+ }
101
+ return "not-found" as const;
102
+ },
103
+ deleteSecureKeyAsync: async (account: string) => {
104
+ if (secureKeyStore.has(account)) {
105
+ secureKeyStore.delete(account);
106
+ return "deleted" as const;
107
+ }
108
+ return "not-found" as const;
109
+ },
110
+ listSecureKeys: () => [...secureKeyStore.keys()],
111
+ getBackendType: () => "encrypted",
112
+ isDowngradedFromKeychain: () => false,
113
+ _resetBackend: () => {},
114
+ _setBackend: () => {},
42
115
  }));
43
116
 
44
117
  mock.module("../tools/credentials/metadata-store.js", () => ({
118
+ assertMetadataWritable: () => {},
45
119
  getCredentialMetadata: () => undefined,
46
120
  upsertCredentialMetadata: () => ({}),
47
121
  listCredentialMetadata: () => [],
122
+ deleteCredentialMetadata: (service: string, field: string): boolean => {
123
+ const idx = metadataStore.findIndex(
124
+ (c) => c.service === service && c.field === field,
125
+ );
126
+ if (idx === -1) return false;
127
+ metadataStore.splice(idx, 1);
128
+ return true;
129
+ },
48
130
  }));
49
131
 
50
132
  mock.module("../util/logger.js", () => ({
@@ -66,7 +148,7 @@ mock.module("../util/logger.js", () => ({
66
148
  // Import the module under test (after mocks are registered)
67
149
  // ---------------------------------------------------------------------------
68
150
 
69
- const { registerOAuthCommand } = await import("../cli/commands/oauth.js");
151
+ const { registerOAuthCommand } = await import("../cli/commands/oauth/index.js");
70
152
 
71
153
  // ---------------------------------------------------------------------------
72
154
  // Test helper
@@ -117,43 +199,61 @@ async function runCli(
117
199
  // Tests
118
200
  // ---------------------------------------------------------------------------
119
201
 
120
- describe("assistant oauth token", () => {
202
+ describe("assistant oauth connections token <provider-key>", () => {
121
203
  beforeEach(() => {
122
204
  mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
205
+ secureKeyStore = new Map();
206
+ metadataStore = [];
207
+ disconnectOAuthProviderCalls = [];
208
+ disconnectOAuthProviderResult = "not-found";
209
+ idCounter = 0;
123
210
  });
124
211
 
125
212
  test("prints bare token in human mode", async () => {
126
- const { exitCode, stdout } = await runCli(["token", "twitter"]);
213
+ const { exitCode, stdout } = await runCli([
214
+ "connections",
215
+ "token",
216
+ "integration:twitter",
217
+ ]);
127
218
  expect(exitCode).toBe(0);
128
219
  expect(stdout).toBe("mock-access-token-xyz\n");
129
220
  });
130
221
 
131
222
  test("prints JSON in --json mode", async () => {
132
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
223
+ const { exitCode, stdout } = await runCli([
224
+ "connections",
225
+ "token",
226
+ "integration:twitter",
227
+ "--json",
228
+ ]);
133
229
  expect(exitCode).toBe(0);
134
230
  const parsed = JSON.parse(stdout);
135
231
  expect(parsed).toEqual({ ok: true, token: "mock-access-token-xyz" });
136
232
  });
137
233
 
138
- test("qualifies service name with integration: prefix", async () => {
234
+ test("passes provider key directly to withValidToken", async () => {
139
235
  let capturedService: string | undefined;
140
236
  mockWithValidToken = async (service, cb) => {
141
237
  capturedService = service;
142
238
  return cb("tok");
143
239
  };
144
240
 
145
- await runCli(["token", "twitter"]);
241
+ await runCli(["connections", "token", "integration:twitter"]);
146
242
  expect(capturedService).toBe("integration:twitter");
147
243
  });
148
244
 
149
- test("works with other service names", async () => {
245
+ test("works with other provider keys", async () => {
150
246
  let capturedService: string | undefined;
151
247
  mockWithValidToken = async (service, cb) => {
152
248
  capturedService = service;
153
249
  return cb("gmail-token");
154
250
  };
155
251
 
156
- const { exitCode, stdout } = await runCli(["token", "gmail"]);
252
+ const { exitCode, stdout } = await runCli([
253
+ "connections",
254
+ "token",
255
+ "integration:gmail",
256
+ ]);
157
257
  expect(exitCode).toBe(0);
158
258
  expect(stdout).toBe("gmail-token\n");
159
259
  expect(capturedService).toBe("integration:gmail");
@@ -166,7 +266,12 @@ describe("assistant oauth token", () => {
166
266
  );
167
267
  };
168
268
 
169
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
269
+ const { exitCode, stdout } = await runCli([
270
+ "connections",
271
+ "token",
272
+ "integration:twitter",
273
+ "--json",
274
+ ]);
170
275
  expect(exitCode).toBe(1);
171
276
  const parsed = JSON.parse(stdout);
172
277
  expect(parsed.ok).toBe(false);
@@ -180,7 +285,12 @@ describe("assistant oauth token", () => {
180
285
  );
181
286
  };
182
287
 
183
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
288
+ const { exitCode, stdout } = await runCli([
289
+ "connections",
290
+ "token",
291
+ "integration:twitter",
292
+ "--json",
293
+ ]);
184
294
  expect(exitCode).toBe(1);
185
295
  const parsed = JSON.parse(stdout);
186
296
  expect(parsed.ok).toBe(false);
@@ -191,13 +301,262 @@ describe("assistant oauth token", () => {
191
301
  // Simulate withValidToken refreshing and returning a new token
192
302
  mockWithValidToken = async (_service, cb) => cb("refreshed-new-token");
193
303
 
194
- const { exitCode, stdout } = await runCli(["token", "twitter"]);
304
+ const { exitCode, stdout } = await runCli([
305
+ "connections",
306
+ "token",
307
+ "integration:twitter",
308
+ ]);
195
309
  expect(exitCode).toBe(0);
196
310
  expect(stdout).toBe("refreshed-new-token\n");
197
311
  });
198
312
 
199
- test("missing service argument exits non-zero", async () => {
200
- const { exitCode } = await runCli(["token"]);
313
+ test("missing provider-key argument exits non-zero", async () => {
314
+ const { exitCode } = await runCli(["connections", "token"]);
201
315
  expect(exitCode).not.toBe(0);
202
316
  });
203
317
  });
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // disconnect
321
+ // ---------------------------------------------------------------------------
322
+
323
+ describe("assistant oauth connections disconnect <provider-key>", () => {
324
+ beforeEach(() => {
325
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
326
+ secureKeyStore = new Map();
327
+ metadataStore = [];
328
+ disconnectOAuthProviderCalls = [];
329
+ disconnectOAuthProviderResult = "not-found";
330
+ idCounter = 0;
331
+ });
332
+
333
+ test("succeeds when an OAuth connection exists", async () => {
334
+ disconnectOAuthProviderResult = "disconnected";
335
+
336
+ const result = await runCli([
337
+ "connections",
338
+ "disconnect",
339
+ "integration:gmail",
340
+ "--json",
341
+ ]);
342
+ expect(result.exitCode).toBe(0);
343
+ const parsed = JSON.parse(result.stdout);
344
+ expect(parsed.ok).toBe(true);
345
+ expect(parsed.service).toBe("integration:gmail");
346
+
347
+ // disconnectOAuthProvider should have been called with the full provider key
348
+ expect(disconnectOAuthProviderCalls).toEqual(["integration:gmail"]);
349
+ });
350
+
351
+ test("reports not-found when nothing exists", async () => {
352
+ const result = await runCli([
353
+ "connections",
354
+ "disconnect",
355
+ "integration:gmail",
356
+ "--json",
357
+ ]);
358
+ expect(result.exitCode).toBe(1);
359
+ const parsed = JSON.parse(result.stdout);
360
+ expect(parsed.ok).toBe(false);
361
+ expect(parsed.error).toContain("No OAuth connection or credentials");
362
+ expect(parsed.error).toContain("integration:gmail");
363
+ });
364
+
365
+ test("cleans up legacy credential keys if present", async () => {
366
+ // Seed legacy credential keys (no OAuth connection)
367
+ const legacyFields = [
368
+ "access_token",
369
+ "refresh_token",
370
+ "client_id",
371
+ "client_secret",
372
+ ];
373
+ for (const field of legacyFields) {
374
+ secureKeyStore.set(
375
+ credentialKey("integration:gmail", field),
376
+ `legacy_${field}_value`,
377
+ );
378
+ metadataStore.push({
379
+ credentialId: nextUUID(),
380
+ service: "integration:gmail",
381
+ field,
382
+ allowedTools: [],
383
+ allowedDomains: [],
384
+ createdAt: Date.now(),
385
+ updatedAt: Date.now(),
386
+ });
387
+ }
388
+
389
+ const result = await runCli([
390
+ "connections",
391
+ "disconnect",
392
+ "integration:gmail",
393
+ "--json",
394
+ ]);
395
+ expect(result.exitCode).toBe(0);
396
+ const parsed = JSON.parse(result.stdout);
397
+ expect(parsed.ok).toBe(true);
398
+ expect(parsed.service).toBe("integration:gmail");
399
+
400
+ // All legacy keys should be removed
401
+ for (const field of legacyFields) {
402
+ expect(
403
+ secureKeyStore.has(credentialKey("integration:gmail", field)),
404
+ ).toBe(false);
405
+ expect(
406
+ metadataStore.find(
407
+ (m) => m.service === "integration:gmail" && m.field === field,
408
+ ),
409
+ ).toBeUndefined();
410
+ }
411
+ });
412
+
413
+ test("cleans up both OAuth connection and legacy keys when both exist", async () => {
414
+ // Seed OAuth connection
415
+ disconnectOAuthProviderResult = "disconnected";
416
+
417
+ // Seed a legacy credential key
418
+ secureKeyStore.set(
419
+ credentialKey("integration:gmail", "access_token"),
420
+ "legacy_token",
421
+ );
422
+ metadataStore.push({
423
+ credentialId: nextUUID(),
424
+ service: "integration:gmail",
425
+ field: "access_token",
426
+ allowedTools: [],
427
+ allowedDomains: [],
428
+ createdAt: Date.now(),
429
+ updatedAt: Date.now(),
430
+ });
431
+
432
+ const result = await runCli([
433
+ "connections",
434
+ "disconnect",
435
+ "integration:gmail",
436
+ "--json",
437
+ ]);
438
+ expect(result.exitCode).toBe(0);
439
+ const parsed = JSON.parse(result.stdout);
440
+ expect(parsed.ok).toBe(true);
441
+
442
+ // Both should be cleaned up
443
+ expect(disconnectOAuthProviderCalls).toEqual(["integration:gmail"]);
444
+ expect(
445
+ secureKeyStore.has(credentialKey("integration:gmail", "access_token")),
446
+ ).toBe(false);
447
+ });
448
+ });
449
+
450
+ // ---------------------------------------------------------------------------
451
+ // providers list
452
+ // ---------------------------------------------------------------------------
453
+
454
+ describe("assistant oauth providers list", () => {
455
+ const fakeProviders = [
456
+ {
457
+ providerKey: "integration:gmail",
458
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
459
+ tokenUrl: "https://oauth2.googleapis.com/token",
460
+ defaultScopes: "[]",
461
+ scopePolicy: "{}",
462
+ extraParams: null,
463
+ createdAt: "2025-01-01T00:00:00.000Z",
464
+ updatedAt: "2025-01-01T00:00:00.000Z",
465
+ },
466
+ {
467
+ providerKey: "integration:google-calendar",
468
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
469
+ tokenUrl: "https://oauth2.googleapis.com/token",
470
+ defaultScopes: "[]",
471
+ scopePolicy: "{}",
472
+ extraParams: null,
473
+ createdAt: "2025-01-01T00:00:00.000Z",
474
+ updatedAt: "2025-01-01T00:00:00.000Z",
475
+ },
476
+ {
477
+ providerKey: "integration:slack",
478
+ authUrl: "https://slack.com/oauth/v2/authorize",
479
+ tokenUrl: "https://slack.com/api/oauth.v2.access",
480
+ defaultScopes: "[]",
481
+ scopePolicy: "{}",
482
+ extraParams: null,
483
+ createdAt: "2025-01-01T00:00:00.000Z",
484
+ updatedAt: "2025-01-01T00:00:00.000Z",
485
+ },
486
+ {
487
+ providerKey: "integration:twitter",
488
+ authUrl: "https://twitter.com/i/oauth2/authorize",
489
+ tokenUrl: "https://api.twitter.com/2/oauth2/token",
490
+ defaultScopes: "[]",
491
+ scopePolicy: "{}",
492
+ extraParams: null,
493
+ createdAt: "2025-01-01T00:00:00.000Z",
494
+ updatedAt: "2025-01-01T00:00:00.000Z",
495
+ },
496
+ ];
497
+
498
+ beforeEach(() => {
499
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
500
+ mockListProviders = () => fakeProviders;
501
+ secureKeyStore = new Map();
502
+ metadataStore = [];
503
+ disconnectOAuthProviderCalls = [];
504
+ disconnectOAuthProviderResult = "not-found";
505
+ idCounter = 0;
506
+ });
507
+
508
+ test("returns all providers when no --provider-key is given", async () => {
509
+ const { exitCode, stdout } = await runCli(["providers", "list", "--json"]);
510
+ expect(exitCode).toBe(0);
511
+ const parsed = JSON.parse(stdout);
512
+ expect(parsed).toHaveLength(4);
513
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
514
+ expect(keys).toContain("integration:gmail");
515
+ expect(keys).toContain("integration:google-calendar");
516
+ expect(keys).toContain("integration:slack");
517
+ expect(keys).toContain("integration:twitter");
518
+ });
519
+
520
+ test("filters by single --provider-key value", async () => {
521
+ const { exitCode, stdout } = await runCli([
522
+ "providers",
523
+ "list",
524
+ "--provider-key",
525
+ "gmail",
526
+ "--json",
527
+ ]);
528
+ expect(exitCode).toBe(0);
529
+ const parsed = JSON.parse(stdout);
530
+ expect(parsed).toHaveLength(1);
531
+ expect(parsed[0].providerKey).toBe("integration:gmail");
532
+ });
533
+
534
+ test("filters by comma-separated OR values", async () => {
535
+ const { exitCode, stdout } = await runCli([
536
+ "providers",
537
+ "list",
538
+ "--provider-key",
539
+ "gmail,google",
540
+ "--json",
541
+ ]);
542
+ expect(exitCode).toBe(0);
543
+ const parsed = JSON.parse(stdout);
544
+ expect(parsed).toHaveLength(2);
545
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
546
+ expect(keys).toContain("integration:gmail");
547
+ expect(keys).toContain("integration:google-calendar");
548
+ });
549
+
550
+ test("returns empty array when comma-separated filter has no matches", async () => {
551
+ const { exitCode, stdout } = await runCli([
552
+ "providers",
553
+ "list",
554
+ "--provider-key",
555
+ "notion,linear",
556
+ "--json",
557
+ ]);
558
+ expect(exitCode).toBe(0);
559
+ const parsed = JSON.parse(stdout);
560
+ expect(parsed).toHaveLength(0);
561
+ });
562
+ });
@@ -1,22 +1,22 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
- getProviderProfile,
4
+ getProviderBehavior,
5
5
  resolveService,
6
- } from "../oauth/provider-profiles.js";
6
+ } from "../oauth/provider-behaviors.js";
7
7
 
8
- describe("oauth provider profiles", () => {
9
- test("gmail profile defines bearer injection templates for Google API hosts", () => {
8
+ describe("oauth provider behaviors", () => {
9
+ test("gmail behavior defines bearer injection templates for Google API hosts", () => {
10
10
  const service = resolveService("gmail");
11
- const profile = getProviderProfile(service);
11
+ const behavior = getProviderBehavior(service);
12
12
 
13
13
  expect(service).toBe("integration:gmail");
14
- expect(profile).toBeDefined();
15
- expect(profile?.injectionTemplates).toBeDefined();
16
- expect(profile?.injectionTemplates).toHaveLength(3);
14
+ expect(behavior).toBeDefined();
15
+ expect(behavior?.injectionTemplates).toBeDefined();
16
+ expect(behavior?.injectionTemplates).toHaveLength(3);
17
17
 
18
18
  const byHost = new Map(
19
- (profile?.injectionTemplates ?? []).map((t) => [t.hostPattern, t]),
19
+ (behavior?.injectionTemplates ?? []).map((t) => [t.hostPattern, t]),
20
20
  );
21
21
 
22
22
  for (const host of [
@@ -1,20 +1,18 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import type { OAuthProviderProfile } from "../oauth/connect-types.js";
3
+ import type { ScopeResolverInput } from "../oauth/scope-policy.js";
4
4
  import { resolveScopes } from "../oauth/scope-policy.js";
5
5
 
6
6
  // ---------------------------------------------------------------------------
7
7
  // Helpers
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
- /** Build a minimal profile for testing scope resolution. */
10
+ /** Build a minimal scope resolver input for testing scope resolution. */
11
11
  function makeProfile(
12
- overrides: Partial<OAuthProviderProfile> = {},
13
- ): OAuthProviderProfile {
12
+ overrides: Partial<ScopeResolverInput> = {},
13
+ ): ScopeResolverInput {
14
14
  return {
15
15
  service: "integration:test-service",
16
- authUrl: "https://auth.example.com/authorize",
17
- tokenUrl: "https://auth.example.com/token",
18
16
  defaultScopes: ["read", "write"],
19
17
  scopePolicy: {
20
18
  allowAdditionalScopes: false,