@vellumai/assistant 0.5.10 → 0.5.11

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 (263) hide show
  1. package/AGENTS.md +8 -0
  2. package/ARCHITECTURE.md +43 -43
  3. package/Dockerfile +2 -0
  4. package/docs/architecture/integrations.md +3 -10
  5. package/docs/architecture/memory.md +7 -12
  6. package/docs/credential-execution-service.md +9 -9
  7. package/docs/skills.md +1 -1
  8. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  9. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  10. package/openapi.yaml +7130 -0
  11. package/package.json +2 -1
  12. package/scripts/generate-openapi.ts +562 -0
  13. package/src/__tests__/acp-session.test.ts +239 -44
  14. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  17. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  18. package/src/__tests__/btw-routes.test.ts +8 -0
  19. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  20. package/src/__tests__/channel-approvals.test.ts +7 -7
  21. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  22. package/src/__tests__/config-schema.test.ts +10 -2
  23. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  24. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  25. package/src/__tests__/conversation-title-service.test.ts +2 -15
  26. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  27. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  28. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  29. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  30. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  31. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  32. package/src/__tests__/heartbeat-service.test.ts +35 -0
  33. package/src/__tests__/host-shell-tool.test.ts +1 -1
  34. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  35. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  36. package/src/__tests__/log-export-workspace.test.ts +1 -1
  37. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  38. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  39. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  40. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  41. package/src/__tests__/memory-regressions.test.ts +53 -42
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  43. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  44. package/src/__tests__/oauth-cli.test.ts +98 -551
  45. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  46. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  47. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  48. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  49. package/src/__tests__/secret-ingress.test.ts +283 -0
  50. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  51. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  52. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  53. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  54. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  55. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  56. package/src/__tests__/skill-memory.test.ts +2 -4
  57. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  58. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  59. package/src/__tests__/skills.test.ts +16 -2
  60. package/src/__tests__/slack-channel-config.test.ts +1 -1
  61. package/src/__tests__/slack-skill.test.ts +5 -69
  62. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  63. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  64. package/src/acp/client-handler.ts +113 -31
  65. package/src/acp/session-manager.ts +29 -27
  66. package/src/approvals/guardian-request-resolvers.ts +1 -1
  67. package/src/cli/AGENTS.md +73 -0
  68. package/src/cli/commands/autonomy.ts +3 -5
  69. package/src/cli/commands/credential-execution.ts +1 -2
  70. package/src/cli/commands/memory.ts +2 -3
  71. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  72. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  73. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  74. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  75. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  76. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  77. package/src/cli/commands/oauth/apps.ts +26 -8
  78. package/src/cli/commands/oauth/connect.ts +373 -0
  79. package/src/cli/commands/oauth/connections.ts +14 -493
  80. package/src/cli/commands/oauth/disconnect.ts +333 -0
  81. package/src/cli/commands/oauth/index.ts +62 -10
  82. package/src/cli/commands/oauth/mode.ts +263 -0
  83. package/src/cli/commands/oauth/ping.ts +222 -0
  84. package/src/cli/commands/oauth/providers.ts +30 -3
  85. package/src/cli/commands/oauth/request.ts +576 -0
  86. package/src/cli/commands/oauth/shared.ts +132 -0
  87. package/src/cli/commands/oauth/status.ts +202 -0
  88. package/src/cli/commands/oauth/token.ts +159 -0
  89. package/src/cli/commands/platform.ts +20 -14
  90. package/src/cli.ts +82 -17
  91. package/src/config/assistant-feature-flags.ts +74 -11
  92. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  93. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  94. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  95. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  96. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  97. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  98. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  99. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  100. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  101. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  102. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  103. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  104. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  105. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  106. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  107. package/src/config/bundled-tool-registry.ts +2 -19
  108. package/src/config/env.ts +5 -1
  109. package/src/config/feature-flag-registry.json +57 -41
  110. package/src/config/loader.ts +4 -0
  111. package/src/config/schemas/platform.ts +0 -8
  112. package/src/config/schemas/security.ts +9 -1
  113. package/src/config/schemas/services.ts +1 -1
  114. package/src/config/skill-state.ts +1 -3
  115. package/src/config/skills.ts +2 -4
  116. package/src/credential-execution/feature-gates.ts +9 -16
  117. package/src/credential-execution/process-manager.ts +12 -0
  118. package/src/daemon/config-watcher.ts +4 -0
  119. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  120. package/src/daemon/conversation-agent-loop.ts +49 -2
  121. package/src/daemon/conversation-memory.ts +0 -1
  122. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  123. package/src/daemon/handlers/conversations.ts +41 -33
  124. package/src/daemon/lifecycle.ts +26 -2
  125. package/src/daemon/message-types/acp.ts +0 -15
  126. package/src/daemon/message-types/memory.ts +0 -1
  127. package/src/daemon/message-types/messages.ts +9 -1
  128. package/src/daemon/message-types/schedules.ts +9 -0
  129. package/src/daemon/server.ts +19 -7
  130. package/src/email/feature-gate.ts +3 -3
  131. package/src/heartbeat/heartbeat-service.ts +48 -0
  132. package/src/inbound/platform-callback-registration.ts +61 -7
  133. package/src/mcp/mcp-oauth-provider.ts +3 -3
  134. package/src/memory/app-store.ts +3 -3
  135. package/src/memory/conversation-crud.ts +124 -0
  136. package/src/memory/conversation-title-service.ts +7 -17
  137. package/src/memory/db-init.ts +8 -0
  138. package/src/memory/embedding-local.ts +47 -2
  139. package/src/memory/indexer.ts +13 -10
  140. package/src/memory/items-extractor.ts +12 -4
  141. package/src/memory/job-utils.ts +5 -0
  142. package/src/memory/jobs-store.ts +10 -2
  143. package/src/memory/journal-memory.ts +6 -2
  144. package/src/memory/llm-request-log-store.ts +88 -21
  145. package/src/memory/memory-recall-log-store.ts +128 -0
  146. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  147. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  148. package/src/memory/migrations/index.ts +2 -0
  149. package/src/memory/retriever.test.ts +4 -5
  150. package/src/memory/schema/infrastructure.ts +31 -0
  151. package/src/memory/schema/oauth.ts +3 -0
  152. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  153. package/src/oauth/connect-orchestrator.ts +54 -0
  154. package/src/oauth/manual-token-connection.ts +5 -5
  155. package/src/oauth/oauth-store.ts +26 -5
  156. package/src/oauth/seed-providers.ts +10 -1
  157. package/src/permissions/checker.ts +2 -2
  158. package/src/permissions/trust-client.ts +2 -2
  159. package/src/platform/client.ts +2 -2
  160. package/src/prompts/journal-context.ts +6 -1
  161. package/src/providers/anthropic/client.ts +143 -1
  162. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  163. package/src/runtime/auth/route-policy.ts +0 -1
  164. package/src/runtime/btw-sidechain.ts +7 -1
  165. package/src/runtime/channel-approvals.ts +2 -2
  166. package/src/runtime/channel-readiness-service.ts +30 -7
  167. package/src/runtime/http-router.ts +31 -0
  168. package/src/runtime/http-server.ts +21 -4
  169. package/src/runtime/http-types.ts +2 -0
  170. package/src/runtime/pending-interactions.ts +21 -3
  171. package/src/runtime/routes/acp-routes.ts +46 -28
  172. package/src/runtime/routes/app-management-routes.ts +123 -0
  173. package/src/runtime/routes/app-routes.ts +31 -0
  174. package/src/runtime/routes/approval-routes.ts +108 -3
  175. package/src/runtime/routes/attachment-routes.ts +45 -0
  176. package/src/runtime/routes/avatar-routes.ts +16 -0
  177. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  178. package/src/runtime/routes/btw-routes.ts +20 -0
  179. package/src/runtime/routes/call-routes.ts +81 -0
  180. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  181. package/src/runtime/routes/channel-routes.ts +18 -0
  182. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  183. package/src/runtime/routes/contact-routes.ts +77 -0
  184. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  185. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  186. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  187. package/src/runtime/routes/conversation-routes.ts +115 -38
  188. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  189. package/src/runtime/routes/debug-routes.ts +23 -0
  190. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  191. package/src/runtime/routes/documents-routes.ts +42 -0
  192. package/src/runtime/routes/events-routes.ts +10 -0
  193. package/src/runtime/routes/global-search-routes.ts +35 -0
  194. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  195. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  196. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  197. package/src/runtime/routes/host-bash-routes.ts +16 -1
  198. package/src/runtime/routes/host-cu-routes.ts +23 -1
  199. package/src/runtime/routes/host-file-routes.ts +18 -1
  200. package/src/runtime/routes/identity-routes.ts +35 -0
  201. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  202. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  203. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  204. package/src/runtime/routes/integrations/twilio.ts +32 -22
  205. package/src/runtime/routes/invite-routes.ts +83 -0
  206. package/src/runtime/routes/log-export-routes.ts +14 -0
  207. package/src/runtime/routes/memory-item-routes.ts +99 -1
  208. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  209. package/src/runtime/routes/migration-routes.ts +40 -0
  210. package/src/runtime/routes/notification-routes.ts +20 -0
  211. package/src/runtime/routes/oauth-apps.ts +11 -3
  212. package/src/runtime/routes/pairing-routes.ts +15 -0
  213. package/src/runtime/routes/recording-routes.ts +72 -0
  214. package/src/runtime/routes/schedule-routes.ts +77 -5
  215. package/src/runtime/routes/secret-routes.ts +63 -1
  216. package/src/runtime/routes/settings-routes.ts +90 -0
  217. package/src/runtime/routes/skills-routes.ts +98 -16
  218. package/src/runtime/routes/subagents-routes.ts +38 -3
  219. package/src/runtime/routes/surface-action-routes.ts +66 -24
  220. package/src/runtime/routes/surface-content-routes.ts +20 -0
  221. package/src/runtime/routes/telemetry-routes.ts +12 -0
  222. package/src/runtime/routes/trace-event-routes.ts +25 -0
  223. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  224. package/src/runtime/routes/tts-routes.ts +15 -4
  225. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  226. package/src/runtime/routes/usage-routes.ts +59 -0
  227. package/src/runtime/routes/watch-routes.ts +28 -0
  228. package/src/runtime/routes/work-items-routes.ts +59 -0
  229. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  230. package/src/runtime/routes/workspace-routes.ts +102 -0
  231. package/src/schedule/scheduler.ts +7 -1
  232. package/src/security/AGENTS.md +7 -0
  233. package/src/security/credential-backend.ts +1 -1
  234. package/src/security/encrypted-store.ts +3 -3
  235. package/src/security/oauth2.ts +55 -0
  236. package/src/security/secret-ingress.ts +174 -0
  237. package/src/security/secret-patterns.ts +133 -0
  238. package/src/security/secret-scanner.ts +28 -117
  239. package/src/signals/confirm.ts +12 -8
  240. package/src/signals/user-message.ts +18 -3
  241. package/src/skills/skill-memory.ts +1 -2
  242. package/src/tasks/task-runner.ts +7 -1
  243. package/src/tools/credentials/broker.ts +1 -1
  244. package/src/tools/credentials/metadata-store.ts +1 -1
  245. package/src/tools/credentials/vault.ts +2 -3
  246. package/src/tools/memory/definitions.ts +1 -1
  247. package/src/tools/memory/handlers.test.ts +2 -4
  248. package/src/tools/skills/load.ts +1 -1
  249. package/src/tools/terminal/safe-env.ts +7 -0
  250. package/src/tools/tool-manifest.ts +1 -1
  251. package/src/util/log-redact.ts +9 -34
  252. package/docs/architecture/keychain-broker.md +0 -68
  253. package/src/cli/commands/oauth/platform.ts +0 -525
  254. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  255. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  256. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  257. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  258. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  259. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  260. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  261. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  262. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  263. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
@@ -0,0 +1,760 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { Command } from "commander";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mock state
7
+ // ---------------------------------------------------------------------------
8
+
9
+ let mockGetProvider: (
10
+ key: string,
11
+ ) => Record<string, unknown> | undefined = () => undefined;
12
+
13
+ let mockGetConnection: (
14
+ id: string,
15
+ ) => Record<string, unknown> | undefined = () => undefined;
16
+
17
+ let mockGetActiveConnection: (
18
+ providerKey: string,
19
+ opts?: { account?: string },
20
+ ) => Record<string, unknown> | undefined = () => undefined;
21
+
22
+ let mockListActiveConnectionsByProvider: (
23
+ providerKey: string,
24
+ ) => Array<Record<string, unknown>> = () => [];
25
+
26
+ let mockDisconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
27
+ "disconnected";
28
+
29
+ let mockDisconnectOAuthProviderCalls: Array<{
30
+ providerKey: string;
31
+ account: string | undefined;
32
+ connectionId: string | undefined;
33
+ }> = [];
34
+
35
+ let mockDeleteSecureKeyViaDaemonCalls: Array<{
36
+ type: string;
37
+ name: string;
38
+ }> = [];
39
+
40
+ let mockDeleteCredentialMetadataCalls: Array<{
41
+ service: string;
42
+ field: string;
43
+ }> = [];
44
+
45
+ let mockIsManagedMode: (key: string) => boolean = () => false;
46
+
47
+ let mockPlatformClientResult: Record<string, unknown> | null = null;
48
+ let mockPlatformFetchResults: Array<{
49
+ ok: boolean;
50
+ status: number;
51
+ body: unknown;
52
+ }> = [];
53
+ let mockPlatformFetchCallIndex = 0;
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Mocks
57
+ // ---------------------------------------------------------------------------
58
+
59
+ mock.module("../../../../config/loader.js", () => ({
60
+ getConfig: () => ({ services: {} }),
61
+ API_KEY_PROVIDERS: [],
62
+ }));
63
+
64
+ mock.module("../../../../oauth/oauth-store.js", () => ({
65
+ getProvider: (key: string) => mockGetProvider(key),
66
+ getConnection: (id: string) => mockGetConnection(id),
67
+ getActiveConnection: (providerKey: string, opts?: { account?: string }) =>
68
+ mockGetActiveConnection(providerKey, opts),
69
+ listActiveConnectionsByProvider: (providerKey: string) =>
70
+ mockListActiveConnectionsByProvider(providerKey),
71
+ disconnectOAuthProvider: async (
72
+ providerKey: string,
73
+ account?: string,
74
+ connectionId?: string,
75
+ ) => {
76
+ mockDisconnectOAuthProviderCalls.push({
77
+ providerKey,
78
+ account,
79
+ connectionId,
80
+ });
81
+ return mockDisconnectOAuthProviderResult;
82
+ },
83
+ getConnectionByProvider: () => undefined,
84
+ listConnections: () => [],
85
+ upsertApp: async () => ({}),
86
+ getApp: () => undefined,
87
+ getAppByProviderAndClientId: () => undefined,
88
+ getMostRecentAppByProvider: () => undefined,
89
+ listApps: () => [],
90
+ deleteApp: async () => false,
91
+ listProviders: () => [],
92
+ registerProvider: () => ({}),
93
+ seedProviders: () => {},
94
+ isProviderConnected: () => false,
95
+ createConnection: () => ({}),
96
+ updateConnection: () => ({}),
97
+ deleteConnection: () => false,
98
+ }));
99
+
100
+ mock.module("../../../../oauth/provider-behaviors.js", () => ({
101
+ resolveService: (service: string) => {
102
+ const aliases: Record<string, string> = {
103
+ gmail: "integration:google",
104
+ google: "integration:google",
105
+ slack: "integration:slack",
106
+ };
107
+ if (aliases[service]) return aliases[service];
108
+ if (!service.includes(":")) return `integration:${service}`;
109
+ return service;
110
+ },
111
+ getProviderBehavior: () => undefined,
112
+ }));
113
+
114
+ mock.module("../../../../oauth/connect-orchestrator.js", () => ({
115
+ orchestrateOAuthConnect: async () => ({
116
+ success: true,
117
+ deferred: false,
118
+ grantedScopes: [],
119
+ }),
120
+ }));
121
+
122
+ mock.module("../../../../platform/client.js", () => ({
123
+ VellumPlatformClient: {
124
+ create: async () => mockPlatformClientResult,
125
+ },
126
+ }));
127
+
128
+ mock.module("../../../../util/browser.js", () => ({
129
+ openInBrowser: () => {},
130
+ }));
131
+
132
+ mock.module("../../../../util/logger.js", () => ({
133
+ getLogger: () => ({
134
+ info: () => {},
135
+ warn: () => {},
136
+ error: () => {},
137
+ debug: () => {},
138
+ }),
139
+ getCliLogger: () => ({
140
+ info: () => {},
141
+ warn: () => {},
142
+ error: () => {},
143
+ debug: () => {},
144
+ }),
145
+ }));
146
+
147
+ mock.module("../../../../tools/credentials/metadata-store.js", () => ({
148
+ deleteCredentialMetadata: (service: string, field: string) => {
149
+ mockDeleteCredentialMetadataCalls.push({ service, field });
150
+ return true;
151
+ },
152
+ getCredentialMetadata: () => undefined,
153
+ upsertCredentialMetadata: () => ({}),
154
+ listCredentialMetadata: () => [],
155
+ assertMetadataWritable: () => {},
156
+ }));
157
+
158
+ mock.module("../../../lib/daemon-credential-client.js", () => ({
159
+ getSecureKeyViaDaemon: async () => undefined,
160
+ deleteSecureKeyViaDaemon: async (type: string, name: string) => {
161
+ mockDeleteSecureKeyViaDaemonCalls.push({ type, name });
162
+ return "deleted" as const;
163
+ },
164
+ }));
165
+
166
+ // Mock shared.js helpers to control managed vs BYO mode routing
167
+ mock.module("../shared.js", () => ({
168
+ resolveService: (service: string) => {
169
+ const aliases: Record<string, string> = {
170
+ gmail: "integration:google",
171
+ google: "integration:google",
172
+ slack: "integration:slack",
173
+ };
174
+ if (aliases[service]) return aliases[service];
175
+ if (!service.includes(":")) return `integration:${service}`;
176
+ return service;
177
+ },
178
+ isManagedMode: (key: string) => mockIsManagedMode(key),
179
+ requirePlatformClient: async (_cmd: Command) => {
180
+ if (
181
+ !mockPlatformClientResult ||
182
+ !(mockPlatformClientResult as Record<string, unknown>).platformAssistantId
183
+ ) {
184
+ process.exitCode = 1;
185
+ process.stdout.write(
186
+ JSON.stringify({
187
+ ok: false,
188
+ error:
189
+ "Platform prerequisites not met (not logged in or missing assistant ID)",
190
+ }) + "\n",
191
+ );
192
+ return null;
193
+ }
194
+ return {
195
+ platformAssistantId: (mockPlatformClientResult as Record<string, unknown>)
196
+ .platformAssistantId,
197
+ fetch: async (): Promise<Response> => {
198
+ const idx = mockPlatformFetchCallIndex++;
199
+ const result = mockPlatformFetchResults[idx] ?? {
200
+ ok: false,
201
+ status: 500,
202
+ body: "mock not configured",
203
+ };
204
+ return {
205
+ ok: result.ok,
206
+ status: result.status,
207
+ json: async () => result.body,
208
+ text: async () =>
209
+ typeof result.body === "string"
210
+ ? result.body
211
+ : JSON.stringify(result.body),
212
+ } as unknown as Response;
213
+ },
214
+ };
215
+ },
216
+ fetchActiveConnections: async (): Promise<Array<
217
+ Record<string, unknown>
218
+ > | null> => {
219
+ const idx = mockPlatformFetchCallIndex++;
220
+ const result = mockPlatformFetchResults[idx];
221
+ if (!result) return [];
222
+ if (!result.ok) return null;
223
+ return result.body as Array<Record<string, unknown>>;
224
+ },
225
+ toBareProvider: (provider: string): string =>
226
+ provider.startsWith("integration:")
227
+ ? provider.slice("integration:".length)
228
+ : provider,
229
+ }));
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Import module under test (after mocks are registered)
233
+ // ---------------------------------------------------------------------------
234
+
235
+ const { registerDisconnectCommand } = await import("../disconnect.js");
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // Test helper
239
+ // ---------------------------------------------------------------------------
240
+
241
+ async function runCommand(
242
+ args: string[],
243
+ ): Promise<{ stdout: string; exitCode: number }> {
244
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
245
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
246
+ const stdoutChunks: string[] = [];
247
+
248
+ process.stdout.write = ((chunk: unknown) => {
249
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
250
+ return true;
251
+ }) as typeof process.stdout.write;
252
+
253
+ process.stderr.write = (() => true) as typeof process.stderr.write;
254
+
255
+ process.exitCode = 0;
256
+
257
+ try {
258
+ const program = new Command();
259
+ program.exitOverride();
260
+ program.option("--json", "JSON output");
261
+ program.configureOutput({
262
+ writeErr: () => {},
263
+ writeOut: (str: string) => stdoutChunks.push(str),
264
+ });
265
+ registerDisconnectCommand(program);
266
+ await program.parseAsync(["node", "assistant", ...args]);
267
+ } catch {
268
+ if (process.exitCode === 0) process.exitCode = 1;
269
+ } finally {
270
+ process.stdout.write = originalStdoutWrite;
271
+ process.stderr.write = originalStderrWrite;
272
+ }
273
+
274
+ const exitCode = process.exitCode ?? 0;
275
+ process.exitCode = 0;
276
+
277
+ return { exitCode, stdout: stdoutChunks.join("") };
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Tests
282
+ // ---------------------------------------------------------------------------
283
+
284
+ describe("assistant oauth disconnect", () => {
285
+ beforeEach(() => {
286
+ mockGetProvider = () => undefined;
287
+ mockGetConnection = () => undefined;
288
+ mockGetActiveConnection = () => undefined;
289
+ mockListActiveConnectionsByProvider = () => [];
290
+ mockDisconnectOAuthProviderResult = "disconnected";
291
+ mockDisconnectOAuthProviderCalls = [];
292
+ mockDeleteSecureKeyViaDaemonCalls = [];
293
+ mockDeleteCredentialMetadataCalls = [];
294
+ mockIsManagedMode = () => false;
295
+ mockPlatformClientResult = null;
296
+ mockPlatformFetchResults = [];
297
+ mockPlatformFetchCallIndex = 0;
298
+ process.exitCode = 0;
299
+ });
300
+
301
+ // -------------------------------------------------------------------------
302
+ // Unknown provider
303
+ // -------------------------------------------------------------------------
304
+
305
+ test("unknown provider returns error with hint", async () => {
306
+ mockGetProvider = () => undefined;
307
+
308
+ const { exitCode, stdout } = await runCommand([
309
+ "disconnect",
310
+ "nonexistent",
311
+ "--json",
312
+ ]);
313
+ expect(exitCode).toBe(1);
314
+ const parsed = JSON.parse(stdout);
315
+ expect(parsed.ok).toBe(false);
316
+ expect(parsed.error).toContain("Unknown provider");
317
+ expect(parsed.error).toContain("providers list");
318
+ });
319
+
320
+ // -------------------------------------------------------------------------
321
+ // Both --account and --connection-id → error
322
+ // -------------------------------------------------------------------------
323
+
324
+ test("both --account and --connection-id returns error", async () => {
325
+ mockGetProvider = () => ({
326
+ providerKey: "integration:google",
327
+ managedServiceConfigKey: null,
328
+ });
329
+
330
+ const { exitCode, stdout } = await runCommand([
331
+ "disconnect",
332
+ "google",
333
+ "--account",
334
+ "user@example.com",
335
+ "--connection-id",
336
+ "conn-123",
337
+ "--json",
338
+ ]);
339
+ expect(exitCode).toBe(1);
340
+ const parsed = JSON.parse(stdout);
341
+ expect(parsed.ok).toBe(false);
342
+ expect(parsed.error).toContain("Cannot specify both");
343
+ expect(parsed.error).toContain("--account");
344
+ expect(parsed.error).toContain("--connection-id");
345
+ });
346
+
347
+ // =========================================================================
348
+ // Managed mode tests
349
+ // =========================================================================
350
+
351
+ describe("managed mode", () => {
352
+ beforeEach(() => {
353
+ mockGetProvider = () => ({
354
+ providerKey: "integration:google",
355
+ managedServiceConfigKey: "google-oauth",
356
+ });
357
+ mockIsManagedMode = () => true;
358
+ mockPlatformClientResult = { platformAssistantId: "asst-123" };
359
+ });
360
+
361
+ test("single connection auto-disconnects", async () => {
362
+ mockPlatformFetchResults = [
363
+ // fetchActiveConnections returns one connection
364
+ {
365
+ ok: true,
366
+ status: 200,
367
+ body: [
368
+ {
369
+ id: "conn-1",
370
+ account_label: "user@gmail.com",
371
+ scopes_granted: ["email"],
372
+ },
373
+ ],
374
+ },
375
+ // disconnect call succeeds
376
+ { ok: true, status: 200, body: {} },
377
+ ];
378
+
379
+ const { exitCode, stdout } = await runCommand([
380
+ "disconnect",
381
+ "google",
382
+ "--json",
383
+ ]);
384
+ expect(exitCode).toBe(0);
385
+ const parsed = JSON.parse(stdout);
386
+ expect(parsed.ok).toBe(true);
387
+ expect(parsed.provider).toBe("integration:google");
388
+ expect(parsed.connectionId).toBe("conn-1");
389
+ expect(parsed.account).toBe("user@gmail.com");
390
+ });
391
+
392
+ test("multiple connections without flag returns error with connection list", async () => {
393
+ mockPlatformFetchResults = [
394
+ {
395
+ ok: true,
396
+ status: 200,
397
+ body: [
398
+ {
399
+ id: "conn-1",
400
+ account_label: "user1@gmail.com",
401
+ scopes_granted: [],
402
+ },
403
+ {
404
+ id: "conn-2",
405
+ account_label: "user2@gmail.com",
406
+ scopes_granted: [],
407
+ },
408
+ ],
409
+ },
410
+ ];
411
+
412
+ const { exitCode, stdout } = await runCommand([
413
+ "disconnect",
414
+ "google",
415
+ "--json",
416
+ ]);
417
+ expect(exitCode).toBe(1);
418
+ const parsed = JSON.parse(stdout);
419
+ expect(parsed.ok).toBe(false);
420
+ expect(parsed.error).toContain("Multiple active connections");
421
+ expect(parsed.error).toContain("--account");
422
+ expect(parsed.error).toContain("--connection-id");
423
+ expect(parsed.connections).toBeDefined();
424
+ expect(parsed.connections).toHaveLength(2);
425
+ });
426
+
427
+ test("--account filters correctly", async () => {
428
+ mockPlatformFetchResults = [
429
+ {
430
+ ok: true,
431
+ status: 200,
432
+ body: [
433
+ {
434
+ id: "conn-1",
435
+ account_label: "user1@gmail.com",
436
+ scopes_granted: [],
437
+ },
438
+ {
439
+ id: "conn-2",
440
+ account_label: "user2@gmail.com",
441
+ scopes_granted: [],
442
+ },
443
+ ],
444
+ },
445
+ // disconnect call succeeds
446
+ { ok: true, status: 200, body: {} },
447
+ ];
448
+
449
+ const { exitCode, stdout } = await runCommand([
450
+ "disconnect",
451
+ "google",
452
+ "--account",
453
+ "user2@gmail.com",
454
+ "--json",
455
+ ]);
456
+ expect(exitCode).toBe(0);
457
+ const parsed = JSON.parse(stdout);
458
+ expect(parsed.ok).toBe(true);
459
+ expect(parsed.connectionId).toBe("conn-2");
460
+ expect(parsed.account).toBe("user2@gmail.com");
461
+ });
462
+
463
+ test("--connection-id validates ownership", async () => {
464
+ mockPlatformFetchResults = [
465
+ {
466
+ ok: true,
467
+ status: 200,
468
+ body: [
469
+ {
470
+ id: "conn-1",
471
+ account_label: "user@gmail.com",
472
+ scopes_granted: [],
473
+ },
474
+ ],
475
+ },
476
+ ];
477
+
478
+ const { exitCode, stdout } = await runCommand([
479
+ "disconnect",
480
+ "google",
481
+ "--connection-id",
482
+ "conn-nonexistent",
483
+ "--json",
484
+ ]);
485
+ expect(exitCode).toBe(1);
486
+ const parsed = JSON.parse(stdout);
487
+ expect(parsed.ok).toBe(false);
488
+ expect(parsed.error).toContain("conn-nonexistent");
489
+ expect(parsed.error).toContain("not an active");
490
+ });
491
+
492
+ test("no connections returns error with hint", async () => {
493
+ mockPlatformFetchResults = [{ ok: true, status: 200, body: [] }];
494
+
495
+ const { exitCode, stdout } = await runCommand([
496
+ "disconnect",
497
+ "google",
498
+ "--json",
499
+ ]);
500
+ expect(exitCode).toBe(1);
501
+ const parsed = JSON.parse(stdout);
502
+ expect(parsed.ok).toBe(false);
503
+ expect(parsed.error).toContain("No active connections");
504
+ expect(parsed.error).toContain("status");
505
+ });
506
+ });
507
+
508
+ // =========================================================================
509
+ // BYO mode tests
510
+ // =========================================================================
511
+
512
+ describe("BYO mode", () => {
513
+ beforeEach(() => {
514
+ mockGetProvider = () => ({
515
+ providerKey: "integration:google",
516
+ managedServiceConfigKey: null,
517
+ });
518
+ mockIsManagedMode = () => false;
519
+ });
520
+
521
+ test("single connection auto-disconnects", async () => {
522
+ mockListActiveConnectionsByProvider = () => [
523
+ {
524
+ id: "conn-1",
525
+ providerKey: "integration:google",
526
+ accountInfo: "user@gmail.com",
527
+ status: "active",
528
+ },
529
+ ];
530
+
531
+ const { exitCode, stdout } = await runCommand([
532
+ "disconnect",
533
+ "google",
534
+ "--json",
535
+ ]);
536
+ expect(exitCode).toBe(0);
537
+ const parsed = JSON.parse(stdout);
538
+ expect(parsed.ok).toBe(true);
539
+ expect(parsed.provider).toBe("integration:google");
540
+ expect(parsed.connectionId).toBe("conn-1");
541
+ expect(parsed.account).toBe("user@gmail.com");
542
+
543
+ // Verify disconnectOAuthProvider was called
544
+ expect(mockDisconnectOAuthProviderCalls).toHaveLength(1);
545
+ expect(mockDisconnectOAuthProviderCalls[0].providerKey).toBe(
546
+ "integration:google",
547
+ );
548
+ expect(mockDisconnectOAuthProviderCalls[0].connectionId).toBe("conn-1");
549
+ });
550
+
551
+ test("single connection also cleans up legacy credential keys", async () => {
552
+ mockListActiveConnectionsByProvider = () => [
553
+ {
554
+ id: "conn-1",
555
+ providerKey: "integration:google",
556
+ accountInfo: "user@gmail.com",
557
+ status: "active",
558
+ },
559
+ ];
560
+
561
+ await runCommand(["disconnect", "google", "--json"]);
562
+
563
+ // Should have attempted to delete legacy keys
564
+ const expectedFields = [
565
+ "access_token",
566
+ "refresh_token",
567
+ "client_id",
568
+ "client_secret",
569
+ ];
570
+ expect(mockDeleteSecureKeyViaDaemonCalls.length).toBe(
571
+ expectedFields.length,
572
+ );
573
+ for (const field of expectedFields) {
574
+ expect(
575
+ mockDeleteSecureKeyViaDaemonCalls.some(
576
+ (c) =>
577
+ c.type === "credential" &&
578
+ c.name === `integration:google:${field}`,
579
+ ),
580
+ ).toBe(true);
581
+ }
582
+
583
+ expect(mockDeleteCredentialMetadataCalls.length).toBe(
584
+ expectedFields.length,
585
+ );
586
+ for (const field of expectedFields) {
587
+ expect(
588
+ mockDeleteCredentialMetadataCalls.some(
589
+ (c) => c.service === "integration:google" && c.field === field,
590
+ ),
591
+ ).toBe(true);
592
+ }
593
+ });
594
+
595
+ test("--account matches accountInfo", async () => {
596
+ mockGetActiveConnection = (providerKey, opts) => {
597
+ if (opts?.account === "user@gmail.com") {
598
+ return {
599
+ id: "conn-1",
600
+ providerKey: "integration:google",
601
+ accountInfo: "user@gmail.com",
602
+ status: "active",
603
+ };
604
+ }
605
+ return undefined;
606
+ };
607
+
608
+ const { exitCode, stdout } = await runCommand([
609
+ "disconnect",
610
+ "google",
611
+ "--account",
612
+ "user@gmail.com",
613
+ "--json",
614
+ ]);
615
+ expect(exitCode).toBe(0);
616
+ const parsed = JSON.parse(stdout);
617
+ expect(parsed.ok).toBe(true);
618
+ expect(parsed.connectionId).toBe("conn-1");
619
+ expect(parsed.account).toBe("user@gmail.com");
620
+ });
621
+
622
+ test("--account with no match returns error", async () => {
623
+ mockGetActiveConnection = () => undefined;
624
+
625
+ const { exitCode, stdout } = await runCommand([
626
+ "disconnect",
627
+ "google",
628
+ "--account",
629
+ "nonexistent@gmail.com",
630
+ "--json",
631
+ ]);
632
+ expect(exitCode).toBe(1);
633
+ const parsed = JSON.parse(stdout);
634
+ expect(parsed.ok).toBe(false);
635
+ expect(parsed.error).toContain("No active connection");
636
+ expect(parsed.error).toContain("nonexistent@gmail.com");
637
+ });
638
+
639
+ test("--connection-id looks up by ID", async () => {
640
+ mockGetConnection = (id) => {
641
+ if (id === "conn-123") {
642
+ return {
643
+ id: "conn-123",
644
+ providerKey: "integration:google",
645
+ accountInfo: "user@gmail.com",
646
+ status: "active",
647
+ };
648
+ }
649
+ return undefined;
650
+ };
651
+
652
+ const { exitCode, stdout } = await runCommand([
653
+ "disconnect",
654
+ "google",
655
+ "--connection-id",
656
+ "conn-123",
657
+ "--json",
658
+ ]);
659
+ expect(exitCode).toBe(0);
660
+ const parsed = JSON.parse(stdout);
661
+ expect(parsed.ok).toBe(true);
662
+ expect(parsed.connectionId).toBe("conn-123");
663
+ });
664
+
665
+ test("--connection-id with wrong provider returns error", async () => {
666
+ mockGetConnection = (id) => {
667
+ if (id === "conn-slack") {
668
+ return {
669
+ id: "conn-slack",
670
+ providerKey: "integration:slack",
671
+ accountInfo: null,
672
+ status: "active",
673
+ };
674
+ }
675
+ return undefined;
676
+ };
677
+
678
+ const { exitCode, stdout } = await runCommand([
679
+ "disconnect",
680
+ "google",
681
+ "--connection-id",
682
+ "conn-slack",
683
+ "--json",
684
+ ]);
685
+ expect(exitCode).toBe(1);
686
+ const parsed = JSON.parse(stdout);
687
+ expect(parsed.ok).toBe(false);
688
+ expect(parsed.error).toContain("conn-slack");
689
+ expect(parsed.error).toContain("not an active");
690
+ });
691
+
692
+ test("multiple connections without flags returns error with list", async () => {
693
+ mockListActiveConnectionsByProvider = () => [
694
+ {
695
+ id: "conn-1",
696
+ providerKey: "integration:google",
697
+ accountInfo: "user1@gmail.com",
698
+ status: "active",
699
+ },
700
+ {
701
+ id: "conn-2",
702
+ providerKey: "integration:google",
703
+ accountInfo: "user2@gmail.com",
704
+ status: "active",
705
+ },
706
+ ];
707
+
708
+ const { exitCode, stdout } = await runCommand([
709
+ "disconnect",
710
+ "google",
711
+ "--json",
712
+ ]);
713
+ expect(exitCode).toBe(1);
714
+ const parsed = JSON.parse(stdout);
715
+ expect(parsed.ok).toBe(false);
716
+ expect(parsed.error).toContain("Multiple active connections");
717
+ expect(parsed.error).toContain("--account");
718
+ expect(parsed.error).toContain("--connection-id");
719
+ expect(parsed.connections).toBeDefined();
720
+ expect(parsed.connections).toHaveLength(2);
721
+ });
722
+
723
+ test("no connections returns error with hint", async () => {
724
+ mockListActiveConnectionsByProvider = () => [];
725
+
726
+ const { exitCode, stdout } = await runCommand([
727
+ "disconnect",
728
+ "google",
729
+ "--json",
730
+ ]);
731
+ expect(exitCode).toBe(1);
732
+ const parsed = JSON.parse(stdout);
733
+ expect(parsed.ok).toBe(false);
734
+ expect(parsed.error).toContain("No active connections");
735
+ expect(parsed.error).toContain("status");
736
+ });
737
+
738
+ test("disconnect error returns error message", async () => {
739
+ mockDisconnectOAuthProviderResult = "error";
740
+ mockListActiveConnectionsByProvider = () => [
741
+ {
742
+ id: "conn-1",
743
+ providerKey: "integration:google",
744
+ accountInfo: null,
745
+ status: "active",
746
+ },
747
+ ];
748
+
749
+ const { exitCode, stdout } = await runCommand([
750
+ "disconnect",
751
+ "google",
752
+ "--json",
753
+ ]);
754
+ expect(exitCode).toBe(1);
755
+ const parsed = JSON.parse(stdout);
756
+ expect(parsed.ok).toBe(false);
757
+ expect(parsed.error).toContain("Failed to disconnect");
758
+ });
759
+ });
760
+ });