@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,467 @@
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 mockIsManagedMode: (key: string) => boolean = () => false;
10
+
11
+ let mockGetActiveConnection: (
12
+ providerKey: string,
13
+ options?: { clientId?: string; account?: string },
14
+ ) => Record<string, unknown> | undefined = () => undefined;
15
+
16
+ let mockWithValidToken: (
17
+ service: string,
18
+ callback: (token: string) => Promise<string>,
19
+ opts?: string | { connectionId: string },
20
+ ) => Promise<string> = async (_service, callback) => callback("mock-token");
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Mocks
24
+ // ---------------------------------------------------------------------------
25
+
26
+ mock.module("../../../../config/loader.js", () => ({
27
+ getConfig: () => ({ services: {} }),
28
+ API_KEY_PROVIDERS: [],
29
+ }));
30
+
31
+ mock.module("../../../../oauth/oauth-store.js", () => ({
32
+ getProvider: () => undefined,
33
+ listConnections: () => [],
34
+ getConnection: () => undefined,
35
+ getConnectionByProvider: () => undefined,
36
+ getActiveConnection: (
37
+ providerKey: string,
38
+ options?: { clientId?: string; account?: string },
39
+ ) => mockGetActiveConnection(providerKey, options),
40
+ listActiveConnectionsByProvider: () => [],
41
+ disconnectOAuthProvider: async () => "not-found" as const,
42
+ upsertApp: async () => ({}),
43
+ getApp: () => undefined,
44
+ getAppByProviderAndClientId: () => undefined,
45
+ getMostRecentAppByProvider: () => undefined,
46
+ listApps: () => [],
47
+ deleteApp: async () => false,
48
+ listProviders: () => [],
49
+ registerProvider: () => ({}),
50
+ seedProviders: () => {},
51
+ isProviderConnected: () => false,
52
+ createConnection: () => ({}),
53
+ updateConnection: () => ({}),
54
+ deleteConnection: () => false,
55
+ }));
56
+
57
+ mock.module("../../../../oauth/provider-behaviors.js", () => ({
58
+ resolveService: (service: string) => {
59
+ const aliases: Record<string, string> = {
60
+ gmail: "integration:google",
61
+ google: "integration:google",
62
+ slack: "integration:slack",
63
+ };
64
+ if (aliases[service]) return aliases[service];
65
+ if (!service.includes(":")) return `integration:${service}`;
66
+ return service;
67
+ },
68
+ getProviderBehavior: () => undefined,
69
+ }));
70
+
71
+ mock.module("../../../../security/token-manager.js", () => ({
72
+ withValidToken: async (
73
+ service: string,
74
+ callback: (token: string) => Promise<string>,
75
+ opts?: string | { connectionId: string },
76
+ ) => mockWithValidToken(service, callback, opts),
77
+ }));
78
+
79
+ mock.module("../../../../util/logger.js", () => ({
80
+ getLogger: () => ({
81
+ info: () => {},
82
+ warn: () => {},
83
+ error: () => {},
84
+ debug: () => {},
85
+ }),
86
+ getCliLogger: () => ({
87
+ info: () => {},
88
+ warn: () => {},
89
+ error: () => {},
90
+ debug: () => {},
91
+ }),
92
+ }));
93
+
94
+ mock.module("../../../lib/daemon-credential-client.js", () => ({
95
+ getSecureKeyViaDaemon: async () => undefined,
96
+ deleteSecureKeyViaDaemon: async () => "not-found" as const,
97
+ }));
98
+
99
+ // Mock shared.js helpers to control managed vs BYO mode routing
100
+ mock.module("../shared.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
+ isManagedMode: (key: string) => mockIsManagedMode(key),
112
+ toBareProvider: (provider: string): string =>
113
+ provider.startsWith("integration:")
114
+ ? provider.slice("integration:".length)
115
+ : provider,
116
+ }));
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Import module under test (after mocks are registered)
120
+ // ---------------------------------------------------------------------------
121
+
122
+ const { registerTokenCommand } = await import("../token.js");
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Test helper
126
+ // ---------------------------------------------------------------------------
127
+
128
+ async function runCommand(
129
+ args: string[],
130
+ ): Promise<{ stdout: string; exitCode: number }> {
131
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
132
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
133
+ const stdoutChunks: string[] = [];
134
+
135
+ process.stdout.write = ((chunk: unknown) => {
136
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
137
+ return true;
138
+ }) as typeof process.stdout.write;
139
+
140
+ process.stderr.write = (() => true) as typeof process.stderr.write;
141
+
142
+ process.exitCode = 0;
143
+
144
+ try {
145
+ const program = new Command();
146
+ program.exitOverride();
147
+ program.option("--json", "JSON output");
148
+ program.configureOutput({
149
+ writeErr: () => {},
150
+ writeOut: (str: string) => stdoutChunks.push(str),
151
+ });
152
+ registerTokenCommand(program);
153
+ await program.parseAsync(["node", "assistant", ...args]);
154
+ } catch {
155
+ if (process.exitCode === 0) process.exitCode = 1;
156
+ } finally {
157
+ process.stdout.write = originalStdoutWrite;
158
+ process.stderr.write = originalStderrWrite;
159
+ }
160
+
161
+ const exitCode = process.exitCode ?? 0;
162
+ process.exitCode = 0;
163
+
164
+ return { exitCode, stdout: stdoutChunks.join("") };
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Tests
169
+ // ---------------------------------------------------------------------------
170
+
171
+ describe("assistant oauth token", () => {
172
+ beforeEach(() => {
173
+ mockIsManagedMode = () => false;
174
+ mockGetActiveConnection = () => undefined;
175
+ mockWithValidToken = async (_service, callback) => callback("mock-token");
176
+ delete process.env.VELLUM_UNTRUSTED_SHELL;
177
+ process.exitCode = 0;
178
+ });
179
+
180
+ // =========================================================================
181
+ // BYO mode — successful token retrieval
182
+ // =========================================================================
183
+
184
+ describe("BYO mode", () => {
185
+ test("returns token in JSON mode", async () => {
186
+ const { exitCode, stdout } = await runCommand([
187
+ "token",
188
+ "google",
189
+ "--json",
190
+ ]);
191
+ expect(exitCode).toBe(0);
192
+ const parsed = JSON.parse(stdout);
193
+ expect(parsed.ok).toBe(true);
194
+ expect(parsed.token).toBe("mock-token");
195
+ });
196
+
197
+ test("prints bare token to stdout in human mode", async () => {
198
+ const { exitCode, stdout } = await runCommand(["token", "google"]);
199
+ expect(exitCode).toBe(0);
200
+ expect(stdout.trim()).toBe("mock-token");
201
+ });
202
+
203
+ test("token refresh failure returns error", async () => {
204
+ mockWithValidToken = async () => {
205
+ throw new Error("Token refresh failed: refresh_token expired");
206
+ };
207
+
208
+ const { exitCode, stdout } = await runCommand([
209
+ "token",
210
+ "google",
211
+ "--json",
212
+ ]);
213
+ expect(exitCode).toBe(1);
214
+ const parsed = JSON.parse(stdout);
215
+ expect(parsed.ok).toBe(false);
216
+ expect(parsed.error).toContain("Token refresh failed");
217
+ });
218
+
219
+ test("no active connection returns error", async () => {
220
+ mockWithValidToken = async () => {
221
+ throw new Error(
222
+ 'No access token found for "integration:google". Authorization required.',
223
+ );
224
+ };
225
+
226
+ const { exitCode, stdout } = await runCommand([
227
+ "token",
228
+ "google",
229
+ "--json",
230
+ ]);
231
+ expect(exitCode).toBe(1);
232
+ const parsed = JSON.parse(stdout);
233
+ expect(parsed.ok).toBe(false);
234
+ expect(parsed.error).toContain("No access token found");
235
+ });
236
+ });
237
+
238
+ // =========================================================================
239
+ // Provider alias resolution
240
+ // =========================================================================
241
+
242
+ test("resolves provider alias (gmail -> integration:google)", async () => {
243
+ let calledWithService = "";
244
+ mockWithValidToken = async (service, callback) => {
245
+ calledWithService = service;
246
+ return callback("alias-token");
247
+ };
248
+
249
+ const { exitCode, stdout } = await runCommand(["token", "gmail", "--json"]);
250
+ expect(exitCode).toBe(0);
251
+ expect(calledWithService).toBe("integration:google");
252
+ const parsed = JSON.parse(stdout);
253
+ expect(parsed.ok).toBe(true);
254
+ expect(parsed.token).toBe("alias-token");
255
+ });
256
+
257
+ // =========================================================================
258
+ // Managed mode — user-friendly error
259
+ // =========================================================================
260
+
261
+ test("managed mode returns user-friendly error", async () => {
262
+ mockIsManagedMode = () => true;
263
+
264
+ const { exitCode, stdout } = await runCommand([
265
+ "token",
266
+ "google",
267
+ "--json",
268
+ ]);
269
+ expect(exitCode).toBe(1);
270
+ const parsed = JSON.parse(stdout);
271
+ expect(parsed.ok).toBe(false);
272
+ expect(parsed.error).toContain("platform-managed");
273
+ expect(parsed.error).toContain("oauth ping");
274
+ expect(parsed.error).toContain("oauth request");
275
+ });
276
+
277
+ // =========================================================================
278
+ // CES shell lockdown
279
+ // =========================================================================
280
+
281
+ test("blocked with VELLUM_UNTRUSTED_SHELL=1", async () => {
282
+ process.env.VELLUM_UNTRUSTED_SHELL = "1";
283
+
284
+ const { exitCode, stdout } = await runCommand([
285
+ "token",
286
+ "google",
287
+ "--json",
288
+ ]);
289
+ expect(exitCode).toBe(1);
290
+ const parsed = JSON.parse(stdout);
291
+ expect(parsed.ok).toBe(false);
292
+ expect(parsed.error).toContain("untrusted shell");
293
+ });
294
+
295
+ test("allowed when VELLUM_UNTRUSTED_SHELL is not set", async () => {
296
+ delete process.env.VELLUM_UNTRUSTED_SHELL;
297
+
298
+ const { exitCode, stdout } = await runCommand([
299
+ "token",
300
+ "google",
301
+ "--json",
302
+ ]);
303
+ expect(exitCode).toBe(0);
304
+ const parsed = JSON.parse(stdout);
305
+ expect(parsed.ok).toBe(true);
306
+ expect(parsed.token).toBe("mock-token");
307
+ });
308
+
309
+ // =========================================================================
310
+ // --account option for BYO disambiguation
311
+ // =========================================================================
312
+
313
+ describe("--account option", () => {
314
+ test("resolves connection by account and uses connectionId", async () => {
315
+ mockGetActiveConnection = (_providerKey, options) => {
316
+ if (options?.account === "user@gmail.com") {
317
+ return {
318
+ id: "conn-abc-123",
319
+ providerKey: "integration:google",
320
+ accountInfo: "user@gmail.com",
321
+ status: "active",
322
+ };
323
+ }
324
+ return undefined;
325
+ };
326
+
327
+ let calledOpts: unknown;
328
+ mockWithValidToken = async (_service, callback, opts) => {
329
+ calledOpts = opts;
330
+ return callback("account-specific-token");
331
+ };
332
+
333
+ const { exitCode, stdout } = await runCommand([
334
+ "token",
335
+ "google",
336
+ "--account",
337
+ "user@gmail.com",
338
+ "--json",
339
+ ]);
340
+ expect(exitCode).toBe(0);
341
+ const parsed = JSON.parse(stdout);
342
+ expect(parsed.ok).toBe(true);
343
+ expect(parsed.token).toBe("account-specific-token");
344
+ expect(calledOpts).toEqual({ connectionId: "conn-abc-123" });
345
+ });
346
+
347
+ test("no matching account returns error", async () => {
348
+ mockGetActiveConnection = () => undefined;
349
+
350
+ const { exitCode, stdout } = await runCommand([
351
+ "token",
352
+ "google",
353
+ "--account",
354
+ "unknown@gmail.com",
355
+ "--json",
356
+ ]);
357
+ expect(exitCode).toBe(1);
358
+ const parsed = JSON.parse(stdout);
359
+ expect(parsed.ok).toBe(false);
360
+ expect(parsed.error).toContain("No active connection found");
361
+ expect(parsed.error).toContain("unknown@gmail.com");
362
+ expect(parsed.error).toContain("oauth connect");
363
+ });
364
+ });
365
+
366
+ // =========================================================================
367
+ // --client-id option for BYO disambiguation
368
+ // =========================================================================
369
+
370
+ describe("--client-id option", () => {
371
+ test("resolves connection by client-id and uses connectionId", async () => {
372
+ mockGetActiveConnection = (_providerKey, options) => {
373
+ if (options?.clientId === "my-client-id") {
374
+ return {
375
+ id: "conn-client-456",
376
+ providerKey: "integration:google",
377
+ accountInfo: null,
378
+ status: "active",
379
+ };
380
+ }
381
+ return undefined;
382
+ };
383
+
384
+ let calledOpts: unknown;
385
+ mockWithValidToken = async (_service, callback, opts) => {
386
+ calledOpts = opts;
387
+ return callback("client-id-token");
388
+ };
389
+
390
+ const { exitCode, stdout } = await runCommand([
391
+ "token",
392
+ "google",
393
+ "--client-id",
394
+ "my-client-id",
395
+ "--json",
396
+ ]);
397
+ expect(exitCode).toBe(0);
398
+ const parsed = JSON.parse(stdout);
399
+ expect(parsed.ok).toBe(true);
400
+ expect(parsed.token).toBe("client-id-token");
401
+ expect(calledOpts).toEqual({ connectionId: "conn-client-456" });
402
+ });
403
+
404
+ test("no matching client-id returns error", async () => {
405
+ mockGetActiveConnection = () => undefined;
406
+
407
+ const { exitCode, stdout } = await runCommand([
408
+ "token",
409
+ "google",
410
+ "--client-id",
411
+ "nonexistent-id",
412
+ "--json",
413
+ ]);
414
+ expect(exitCode).toBe(1);
415
+ const parsed = JSON.parse(stdout);
416
+ expect(parsed.ok).toBe(false);
417
+ expect(parsed.error).toContain("No active connection found");
418
+ expect(parsed.error).toContain("nonexistent-id");
419
+ });
420
+ });
421
+
422
+ // =========================================================================
423
+ // JSON vs human output
424
+ // =========================================================================
425
+
426
+ test("JSON output includes ok and token fields", async () => {
427
+ const { exitCode, stdout } = await runCommand([
428
+ "token",
429
+ "google",
430
+ "--json",
431
+ ]);
432
+ expect(exitCode).toBe(0);
433
+ const parsed = JSON.parse(stdout);
434
+ expect(parsed).toHaveProperty("ok", true);
435
+ expect(parsed).toHaveProperty("token");
436
+ expect(typeof parsed.token).toBe("string");
437
+ });
438
+
439
+ test("human output prints bare token without JSON wrapper", async () => {
440
+ mockWithValidToken = async (_service, callback) =>
441
+ callback("bare-token-value");
442
+
443
+ const { exitCode, stdout } = await runCommand(["token", "google"]);
444
+ expect(exitCode).toBe(0);
445
+ // Human mode should NOT contain JSON structure
446
+ expect(stdout).not.toContain("{");
447
+ expect(stdout).not.toContain('"ok"');
448
+ expect(stdout.trim()).toBe("bare-token-value");
449
+ });
450
+
451
+ test("JSON error output includes ok and error fields", async () => {
452
+ mockWithValidToken = async () => {
453
+ throw new Error("Something went wrong");
454
+ };
455
+
456
+ const { exitCode, stdout } = await runCommand([
457
+ "token",
458
+ "google",
459
+ "--json",
460
+ ]);
461
+ expect(exitCode).toBe(1);
462
+ const parsed = JSON.parse(stdout);
463
+ expect(parsed).toHaveProperty("ok", false);
464
+ expect(parsed).toHaveProperty("error");
465
+ expect(typeof parsed.error).toBe("string");
466
+ });
467
+ });
@@ -97,9 +97,15 @@ Examples:
97
97
  .description(
98
98
  "Look up an OAuth app by ID, provider + client-id, or provider",
99
99
  )
100
- .option("--id <id>", "App ID (UUID)")
101
- .option("--provider <key>", "Provider key (e.g. integration:google)")
102
- .option("--client-id <id>", "OAuth client ID (requires --provider)")
100
+ .option("--id <id>", "App ID (UUID) from 'assistant oauth apps list'")
101
+ .option(
102
+ "--provider <key>",
103
+ "Provider key (e.g. integration:google) from 'assistant oauth providers list'",
104
+ )
105
+ .option(
106
+ "--client-id <id>",
107
+ "OAuth client ID (requires --provider). Find registered client IDs via 'assistant oauth apps list'.",
108
+ )
103
109
  .addHelpText(
104
110
  "after",
105
111
  `
@@ -133,14 +139,23 @@ At least --id or --provider must be specified.`,
133
139
  } else {
134
140
  writeOutput(cmd, {
135
141
  ok: false,
136
- error: "Provide --id, --provider, or --provider + --client-id",
142
+ error:
143
+ "Provide --id, --provider, or --provider + --client-id. Run 'assistant oauth apps list' to see all registered apps.",
137
144
  });
138
145
  process.exitCode = 1;
139
146
  return;
140
147
  }
141
148
 
142
149
  if (!row) {
143
- writeOutput(cmd, { ok: false, error: "App not found" });
150
+ const lookup = opts.id
151
+ ? `id=${opts.id}`
152
+ : opts.provider && opts.clientId
153
+ ? `provider=${opts.provider}, clientId=${opts.clientId}`
154
+ : `provider=${opts.provider}`;
155
+ writeOutput(cmd, {
156
+ ok: false,
157
+ error: `No app found for ${lookup}. Run 'assistant oauth apps list' to see registered apps, or 'assistant oauth apps upsert --help' to register a new one.`,
158
+ });
144
159
  process.exitCode = 1;
145
160
  return;
146
161
  }
@@ -163,9 +178,12 @@ At least --id or --provider must be specified.`,
163
178
  .description("Create or return an existing OAuth app registration")
164
179
  .requiredOption(
165
180
  "--provider <key>",
166
- "Provider key (e.g. integration:google)",
181
+ "Provider key (e.g. integration:google) from 'assistant oauth providers list'",
182
+ )
183
+ .requiredOption(
184
+ "--client-id <id>",
185
+ "OAuth client ID from the provider's developer console",
167
186
  )
168
- .requiredOption("--client-id <id>", "OAuth client ID")
169
187
  .option(
170
188
  "--client-secret <secret>",
171
189
  "OAuth client secret (stored in credential store)",
@@ -293,7 +311,7 @@ Examples:
293
311
  if (!deleted) {
294
312
  writeOutput(cmd, {
295
313
  ok: false,
296
- error: `App not found: ${id}`,
314
+ error: `App not found: ${id}. Run 'assistant oauth apps list' to see registered apps and their IDs.`,
297
315
  });
298
316
  process.exitCode = 1;
299
317
  return;