@vellumai/assistant 0.5.9 → 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 (278) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +48 -48
  3. package/Dockerfile +2 -0
  4. package/README.md +1 -1
  5. package/docs/architecture/integrations.md +6 -13
  6. package/docs/architecture/memory.md +7 -12
  7. package/docs/architecture/security.md +5 -5
  8. package/docs/credential-execution-service.md +9 -9
  9. package/docs/skills.md +1 -1
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7130 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/channel-approvals.test.ts +7 -7
  23. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  24. package/src/__tests__/config-schema.test.ts +10 -2
  25. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  26. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  27. package/src/__tests__/conversation-title-service.test.ts +2 -15
  28. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  29. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  30. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  31. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  32. package/src/__tests__/credentials-cli.test.ts +3 -3
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  34. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  35. package/src/__tests__/heartbeat-service.test.ts +35 -0
  36. package/src/__tests__/host-shell-tool.test.ts +1 -1
  37. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  38. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  39. package/src/__tests__/log-export-workspace.test.ts +1 -1
  40. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  42. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  43. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  44. package/src/__tests__/memory-regressions.test.ts +53 -42
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  46. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  47. package/src/__tests__/oauth-cli.test.ts +98 -551
  48. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  49. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  50. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  51. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  52. package/src/__tests__/secret-ingress.test.ts +283 -0
  53. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  54. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  55. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  56. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  57. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  58. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  59. package/src/__tests__/skill-memory.test.ts +2 -4
  60. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  61. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  62. package/src/__tests__/skills.test.ts +16 -2
  63. package/src/__tests__/slack-channel-config.test.ts +1 -1
  64. package/src/__tests__/slack-skill.test.ts +5 -69
  65. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  66. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
  67. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
  68. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  69. package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
  70. package/src/acp/client-handler.ts +113 -31
  71. package/src/acp/session-manager.ts +29 -27
  72. package/src/approvals/guardian-request-resolvers.ts +1 -1
  73. package/src/cli/AGENTS.md +73 -0
  74. package/src/cli/commands/autonomy.ts +3 -5
  75. package/src/cli/commands/credential-execution.ts +1 -2
  76. package/src/cli/commands/credentials.ts +4 -4
  77. package/src/cli/commands/memory.ts +2 -3
  78. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  79. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  80. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  81. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  82. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  83. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  84. package/src/cli/commands/oauth/apps.ts +29 -11
  85. package/src/cli/commands/oauth/connect.ts +373 -0
  86. package/src/cli/commands/oauth/connections.ts +14 -493
  87. package/src/cli/commands/oauth/disconnect.ts +333 -0
  88. package/src/cli/commands/oauth/index.ts +62 -10
  89. package/src/cli/commands/oauth/mode.ts +263 -0
  90. package/src/cli/commands/oauth/ping.ts +222 -0
  91. package/src/cli/commands/oauth/providers.ts +30 -3
  92. package/src/cli/commands/oauth/request.ts +576 -0
  93. package/src/cli/commands/oauth/shared.ts +132 -0
  94. package/src/cli/commands/oauth/status.ts +202 -0
  95. package/src/cli/commands/oauth/token.ts +159 -0
  96. package/src/cli/commands/platform.ts +20 -14
  97. package/src/cli.ts +82 -17
  98. package/src/config/assistant-feature-flags.ts +74 -11
  99. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  100. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  101. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  102. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  103. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  104. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  105. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  106. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  107. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  108. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  109. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  111. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  112. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  113. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  114. package/src/config/bundled-tool-registry.ts +2 -19
  115. package/src/config/env.ts +5 -1
  116. package/src/config/feature-flag-registry.json +57 -41
  117. package/src/config/loader.ts +4 -0
  118. package/src/config/schemas/platform.ts +0 -8
  119. package/src/config/schemas/security.ts +9 -1
  120. package/src/config/schemas/services.ts +1 -1
  121. package/src/config/skill-state.ts +1 -3
  122. package/src/config/skills.ts +2 -4
  123. package/src/credential-execution/feature-gates.ts +9 -16
  124. package/src/credential-execution/process-manager.ts +12 -0
  125. package/src/daemon/config-watcher.ts +4 -0
  126. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  127. package/src/daemon/conversation-agent-loop.ts +49 -2
  128. package/src/daemon/conversation-memory.ts +0 -1
  129. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  130. package/src/daemon/handlers/conversations.ts +41 -33
  131. package/src/daemon/lifecycle.ts +28 -5
  132. package/src/daemon/message-types/acp.ts +0 -15
  133. package/src/daemon/message-types/memory.ts +0 -1
  134. package/src/daemon/message-types/messages.ts +9 -1
  135. package/src/daemon/message-types/schedules.ts +9 -0
  136. package/src/daemon/server.ts +19 -7
  137. package/src/email/feature-gate.ts +3 -3
  138. package/src/heartbeat/heartbeat-service.ts +48 -0
  139. package/src/inbound/platform-callback-registration.ts +61 -7
  140. package/src/mcp/mcp-oauth-provider.ts +3 -3
  141. package/src/memory/app-store.ts +3 -3
  142. package/src/memory/conversation-crud.ts +124 -0
  143. package/src/memory/conversation-title-service.ts +7 -17
  144. package/src/memory/db-init.ts +8 -0
  145. package/src/memory/embedding-local.ts +47 -2
  146. package/src/memory/indexer.ts +13 -10
  147. package/src/memory/items-extractor.ts +12 -4
  148. package/src/memory/job-utils.ts +5 -0
  149. package/src/memory/jobs-store.ts +10 -2
  150. package/src/memory/journal-memory.ts +6 -2
  151. package/src/memory/llm-request-log-store.ts +88 -21
  152. package/src/memory/memory-recall-log-store.ts +128 -0
  153. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  154. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  155. package/src/memory/migrations/index.ts +2 -0
  156. package/src/memory/migrations/validate-migration-state.ts +14 -1
  157. package/src/memory/retriever.test.ts +4 -5
  158. package/src/memory/schema/infrastructure.ts +31 -0
  159. package/src/memory/schema/oauth.ts +3 -0
  160. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  161. package/src/oauth/connect-orchestrator.ts +54 -0
  162. package/src/oauth/manual-token-connection.ts +5 -5
  163. package/src/oauth/oauth-store.ts +26 -5
  164. package/src/oauth/seed-providers.ts +10 -1
  165. package/src/permissions/checker.ts +2 -2
  166. package/src/permissions/trust-client.ts +2 -2
  167. package/src/platform/client.ts +2 -2
  168. package/src/prompts/journal-context.ts +6 -1
  169. package/src/providers/anthropic/client.ts +143 -1
  170. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  171. package/src/runtime/auth/route-policy.ts +0 -1
  172. package/src/runtime/btw-sidechain.ts +7 -1
  173. package/src/runtime/channel-approvals.ts +2 -2
  174. package/src/runtime/channel-readiness-service.ts +30 -7
  175. package/src/runtime/http-router.ts +31 -0
  176. package/src/runtime/http-server.ts +21 -4
  177. package/src/runtime/http-types.ts +2 -0
  178. package/src/runtime/pending-interactions.ts +21 -3
  179. package/src/runtime/routes/acp-routes.ts +46 -28
  180. package/src/runtime/routes/app-management-routes.ts +123 -0
  181. package/src/runtime/routes/app-routes.ts +31 -0
  182. package/src/runtime/routes/approval-routes.ts +108 -3
  183. package/src/runtime/routes/attachment-routes.ts +45 -0
  184. package/src/runtime/routes/avatar-routes.ts +16 -0
  185. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  186. package/src/runtime/routes/btw-routes.ts +20 -0
  187. package/src/runtime/routes/call-routes.ts +81 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  189. package/src/runtime/routes/channel-routes.ts +18 -0
  190. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  191. package/src/runtime/routes/contact-routes.ts +77 -0
  192. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  193. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  194. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  195. package/src/runtime/routes/conversation-routes.ts +115 -38
  196. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  197. package/src/runtime/routes/debug-routes.ts +23 -0
  198. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  199. package/src/runtime/routes/documents-routes.ts +42 -0
  200. package/src/runtime/routes/events-routes.ts +10 -0
  201. package/src/runtime/routes/global-search-routes.ts +35 -0
  202. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  203. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  204. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  205. package/src/runtime/routes/host-bash-routes.ts +16 -1
  206. package/src/runtime/routes/host-cu-routes.ts +23 -1
  207. package/src/runtime/routes/host-file-routes.ts +18 -1
  208. package/src/runtime/routes/identity-routes.ts +35 -0
  209. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  210. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  211. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  212. package/src/runtime/routes/integrations/twilio.ts +32 -22
  213. package/src/runtime/routes/invite-routes.ts +83 -0
  214. package/src/runtime/routes/log-export-routes.ts +14 -0
  215. package/src/runtime/routes/memory-item-routes.ts +99 -1
  216. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  217. package/src/runtime/routes/migration-routes.ts +40 -0
  218. package/src/runtime/routes/notification-routes.ts +20 -0
  219. package/src/runtime/routes/oauth-apps.ts +11 -3
  220. package/src/runtime/routes/pairing-routes.ts +15 -0
  221. package/src/runtime/routes/recording-routes.ts +72 -0
  222. package/src/runtime/routes/schedule-routes.ts +77 -5
  223. package/src/runtime/routes/secret-routes.ts +63 -1
  224. package/src/runtime/routes/settings-routes.ts +91 -1
  225. package/src/runtime/routes/skills-routes.ts +98 -16
  226. package/src/runtime/routes/subagents-routes.ts +38 -3
  227. package/src/runtime/routes/surface-action-routes.ts +66 -24
  228. package/src/runtime/routes/surface-content-routes.ts +20 -0
  229. package/src/runtime/routes/telemetry-routes.ts +12 -0
  230. package/src/runtime/routes/trace-event-routes.ts +25 -0
  231. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  232. package/src/runtime/routes/tts-routes.ts +15 -4
  233. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  234. package/src/runtime/routes/usage-routes.ts +59 -0
  235. package/src/runtime/routes/watch-routes.ts +28 -0
  236. package/src/runtime/routes/work-items-routes.ts +59 -0
  237. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  238. package/src/runtime/routes/workspace-routes.ts +102 -0
  239. package/src/schedule/scheduler.ts +7 -1
  240. package/src/security/AGENTS.md +7 -0
  241. package/src/security/credential-backend.ts +1 -1
  242. package/src/security/encrypted-store.ts +3 -3
  243. package/src/security/oauth2.ts +55 -0
  244. package/src/security/secret-ingress.ts +174 -0
  245. package/src/security/secret-patterns.ts +133 -0
  246. package/src/security/secret-scanner.ts +28 -117
  247. package/src/signals/confirm.ts +12 -8
  248. package/src/signals/user-message.ts +18 -3
  249. package/src/skills/skill-memory.ts +1 -2
  250. package/src/tasks/task-runner.ts +7 -1
  251. package/src/tools/credentials/broker.ts +1 -1
  252. package/src/tools/credentials/metadata-store.ts +1 -1
  253. package/src/tools/credentials/vault.ts +2 -3
  254. package/src/tools/memory/definitions.ts +1 -1
  255. package/src/tools/memory/handlers.test.ts +2 -4
  256. package/src/tools/skills/load.ts +1 -1
  257. package/src/tools/terminal/safe-env.ts +7 -0
  258. package/src/tools/tool-manifest.ts +1 -1
  259. package/src/util/log-redact.ts +9 -34
  260. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
  261. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
  262. package/src/workspace/migrations/AGENTS.md +11 -0
  263. package/src/workspace/migrations/runner.ts +16 -6
  264. package/src/workspace/migrations/types.ts +7 -0
  265. package/docs/architecture/keychain-broker.md +0 -69
  266. package/src/__tests__/keychain-broker-client.test.ts +0 -800
  267. package/src/cli/commands/oauth/platform.ts +0 -525
  268. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  269. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  270. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  271. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  272. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  273. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  274. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  275. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  276. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  277. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  278. package/src/security/keychain-broker-client.ts +0 -446
@@ -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,12 +178,15 @@ 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
- "OAuth client secret (stored in secure keychain)",
189
+ "OAuth client secret (stored in credential store)",
172
190
  )
173
191
  .option(
174
192
  "--client-secret-credential-path <path>",
@@ -179,7 +197,7 @@ At least --id or --provider must be specified.`,
179
197
  `
180
198
  Creates a new app registration or returns the existing one if an app with the
181
199
  same provider and client ID already exists. The client secret, if provided, is
182
- stored in the secure system keychain — not in the database.
200
+ stored in the secure credential store — not in the database.
183
201
 
184
202
  When an existing app is matched and a --client-secret is provided, the stored
185
203
  secret is updated. The app row itself is returned as-is.
@@ -277,7 +295,7 @@ Arguments:
277
295
  id The app UUID to delete (as returned by "apps list" or "apps get")
278
296
 
279
297
  Permanently removes the app registration and its stored client secret from
280
- the keychain. Any OAuth connections that reference this app will no longer be
298
+ the credential store. Any OAuth connections that reference this app will no longer be
281
299
  able to refresh tokens.
282
300
 
283
301
  Exits with code 1 if the app ID is not found.
@@ -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;