@vellumai/assistant 0.5.11 → 0.5.13

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 (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -0,0 +1,246 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ const testDir = mkdtempSync(join(tmpdir(), "platform-status-test-"));
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Mock state
10
+ // ---------------------------------------------------------------------------
11
+
12
+ let mockGetSecureKeyViaDaemon: (
13
+ account: string,
14
+ ) => Promise<string | undefined> = async () => undefined;
15
+
16
+ let mockResolvePlatformCallbackRegistrationContext: () => Promise<
17
+ Record<string, unknown>
18
+ > = async () => ({
19
+ containerized: false,
20
+ platformBaseUrl: "",
21
+ assistantId: "",
22
+ hasInternalApiKey: false,
23
+ hasAssistantApiKey: false,
24
+ authHeader: null,
25
+ enabled: false,
26
+ });
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Mocks
30
+ // ---------------------------------------------------------------------------
31
+
32
+ mock.module("../../../../inbound/platform-callback-registration.js", () => ({
33
+ resolvePlatformCallbackRegistrationContext: () =>
34
+ mockResolvePlatformCallbackRegistrationContext(),
35
+ registerCallbackRoute: async () => "",
36
+ shouldUsePlatformCallbacks: () => false,
37
+ resolveCallbackUrl: async () => "",
38
+ }));
39
+
40
+ mock.module("../../../lib/daemon-credential-client.js", () => ({
41
+ getSecureKeyViaDaemon: (account: string) =>
42
+ mockGetSecureKeyViaDaemon(account),
43
+ deleteSecureKeyViaDaemon: async () => "not-found" as const,
44
+ setSecureKeyViaDaemon: async () => false,
45
+ getProviderKeyViaDaemon: async () => undefined,
46
+ getSecureKeyResultViaDaemon: async () => ({
47
+ value: undefined,
48
+ unreachable: false,
49
+ }),
50
+ }));
51
+
52
+ mock.module("../../../../util/logger.js", () => ({
53
+ getLogger: () => ({
54
+ info: () => {},
55
+ warn: () => {},
56
+ error: () => {},
57
+ debug: () => {},
58
+ }),
59
+ getCliLogger: () => ({
60
+ info: () => {},
61
+ warn: () => {},
62
+ error: () => {},
63
+ debug: () => {},
64
+ }),
65
+ initLogger: () => {},
66
+ truncateForLog: (value: string, maxLen = 500) =>
67
+ value.length > maxLen ? value.slice(0, maxLen) + "..." : value,
68
+ pruneOldLogFiles: () => 0,
69
+ }));
70
+
71
+ mock.module("../../../../util/platform.js", () => ({
72
+ getRootDir: () => testDir,
73
+ getDataDir: () => join(testDir, "data"),
74
+ getWorkspaceSkillsDir: () => join(testDir, "skills"),
75
+ getWorkspaceDir: () => join(testDir, "workspace"),
76
+ getWorkspaceHooksDir: () => join(testDir, "workspace", "hooks"),
77
+ getWorkspaceConfigPath: () => join(testDir, "workspace", "config.json"),
78
+ getHooksDir: () => join(testDir, "hooks"),
79
+ getSignalsDir: () => join(testDir, "signals"),
80
+ getConversationsDir: () => join(testDir, "conversations"),
81
+ getEmbeddingModelsDir: () => join(testDir, "models"),
82
+ getSandboxRootDir: () => join(testDir, "sandbox"),
83
+ getSandboxWorkingDir: () => join(testDir, "sandbox", "work"),
84
+ getInterfacesDir: () => join(testDir, "interfaces"),
85
+ getSoundsDir: () => join(testDir, "sounds"),
86
+ getHistoryPath: () => join(testDir, "history"),
87
+ isMacOS: () => process.platform === "darwin",
88
+ isLinux: () => process.platform === "linux",
89
+ isWindows: () => process.platform === "win32",
90
+ getPlatformName: () => "linux",
91
+ getClipboardCommand: () => null,
92
+ resolveInstanceDataDir: () => undefined,
93
+ normalizeAssistantId: (id: string) => id,
94
+ getTCPPort: () => 0,
95
+ isTCPEnabled: () => false,
96
+ getTCPHost: () => "127.0.0.1",
97
+ isIOSPairingEnabled: () => false,
98
+ getPlatformTokenPath: () => join(testDir, "token"),
99
+ readPlatformToken: () => null,
100
+ getPidPath: () => join(testDir, "test.pid"),
101
+ getDbPath: () => join(testDir, "test.db"),
102
+ getLogPath: () => join(testDir, "test.log"),
103
+ getWorkspaceDirDisplay: () => testDir,
104
+ getWorkspacePromptPath: (file: string) => join(testDir, file),
105
+ ensureDataDir: () => {},
106
+ }));
107
+
108
+ mock.module("../../../../config/loader.js", () => ({
109
+ API_KEY_PROVIDERS: [] as const,
110
+ getConfig: () => ({
111
+ permissions: { mode: "workspace" },
112
+ skills: { load: { extraDirs: [] } },
113
+ sandbox: { enabled: true },
114
+ }),
115
+ loadConfig: () => ({}),
116
+ invalidateConfigCache: () => {},
117
+ saveConfig: () => {},
118
+ loadRawConfig: () => ({}),
119
+ saveRawConfig: () => {},
120
+ getNestedValue: () => undefined,
121
+ setNestedValue: () => {},
122
+ applyNestedDefaults: (config: unknown) => config,
123
+ deepMergeMissing: () => false,
124
+ deepMergeOverwrite: () => {},
125
+ mergeDefaultWorkspaceConfig: () => {},
126
+ }));
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Import module under test (after mocks are registered)
130
+ // ---------------------------------------------------------------------------
131
+
132
+ const { buildCliProgram } = await import("../../../program.js");
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Test helper
136
+ // ---------------------------------------------------------------------------
137
+
138
+ async function runCommand(
139
+ args: string[],
140
+ ): Promise<{ stdout: string; exitCode: number }> {
141
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
142
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
143
+ const stdoutChunks: string[] = [];
144
+
145
+ process.stdout.write = ((chunk: unknown) => {
146
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
147
+ return true;
148
+ }) as typeof process.stdout.write;
149
+
150
+ process.stderr.write = (() => true) as typeof process.stderr.write;
151
+
152
+ process.exitCode = 0;
153
+
154
+ try {
155
+ const program = buildCliProgram();
156
+ program.exitOverride();
157
+ program.configureOutput({
158
+ writeErr: () => {},
159
+ writeOut: (str: string) => stdoutChunks.push(str),
160
+ });
161
+ await program.parseAsync(["node", "assistant", ...args]);
162
+ } catch {
163
+ if (process.exitCode === 0) process.exitCode = 1;
164
+ } finally {
165
+ process.stdout.write = originalStdoutWrite;
166
+ process.stderr.write = originalStderrWrite;
167
+ }
168
+
169
+ const exitCode = process.exitCode ?? 0;
170
+ process.exitCode = 0;
171
+
172
+ return { exitCode, stdout: stdoutChunks.join("") };
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Tests
177
+ // ---------------------------------------------------------------------------
178
+
179
+ describe("assistant platform status", () => {
180
+ beforeEach(() => {
181
+ mockGetSecureKeyViaDaemon = async () => undefined;
182
+ mockResolvePlatformCallbackRegistrationContext = async () => ({
183
+ containerized: false,
184
+ platformBaseUrl: "",
185
+ assistantId: "",
186
+ hasInternalApiKey: false,
187
+ hasAssistantApiKey: false,
188
+ authHeader: null,
189
+ enabled: false,
190
+ });
191
+ process.exitCode = 0;
192
+ });
193
+
194
+ test("connected platform returns full status with stored credentials", async () => {
195
+ /**
196
+ * When the assistant has stored platform credentials and a valid
197
+ * registration context, the status command should report connected
198
+ * with all context fields populated.
199
+ */
200
+
201
+ // GIVEN a containerized environment with platform configuration
202
+ mockResolvePlatformCallbackRegistrationContext = async () => ({
203
+ containerized: true,
204
+ platformBaseUrl: "https://platform.vellum.ai",
205
+ assistantId: "asst-abc-123",
206
+ hasInternalApiKey: true,
207
+ hasAssistantApiKey: true,
208
+ authHeader: "Bearer internal-key",
209
+ enabled: true,
210
+ });
211
+
212
+ // AND stored platform credentials exist
213
+ mockGetSecureKeyViaDaemon = async (account: string) => {
214
+ if (account === "credential/vellum/platform_base_url")
215
+ return "https://platform.vellum.ai";
216
+ if (account === "credential/vellum/assistant_api_key")
217
+ return "sk-test-key";
218
+ if (account === "credential/vellum/platform_organization_id")
219
+ return "org-456";
220
+ if (account === "credential/vellum/platform_user_id") return "user-789";
221
+ return undefined;
222
+ };
223
+
224
+ // WHEN the status command is run with --json
225
+ const { exitCode, stdout } = await runCommand([
226
+ "platform",
227
+ "status",
228
+ "--json",
229
+ ]);
230
+
231
+ // THEN the command succeeds
232
+ expect(exitCode).toBe(0);
233
+
234
+ // AND the output contains the expected status fields
235
+ const parsed = JSON.parse(stdout);
236
+ expect(parsed.containerized).toBe(true);
237
+ expect(parsed.baseUrl).toBe("https://platform.vellum.ai");
238
+ expect(parsed.assistantId).toBe("asst-abc-123");
239
+ expect(parsed.hasInternalApiKey).toBe(true);
240
+ expect(parsed.hasAssistantApiKey).toBe(true);
241
+ expect(parsed.available).toBe(true);
242
+ expect(parsed.connected).toBe(true);
243
+ expect(parsed.organizationId).toBe("org-456");
244
+ expect(parsed.userId).toBe("user-789");
245
+ });
246
+ });
@@ -0,0 +1,104 @@
1
+ import type { Command } from "commander";
2
+
3
+ import { credentialKey } from "../../../security/credential-key.js";
4
+ import { getSecureKeyViaDaemon } from "../../lib/daemon-credential-client.js";
5
+ import { getCliLogger } from "../../logger.js";
6
+ import { shouldOutputJson, writeOutput } from "../../output.js";
7
+
8
+ const log = getCliLogger("cli");
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Credential store keys
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const CREDENTIAL_KEYS = {
15
+ baseUrl: { service: "vellum", field: "platform_base_url" },
16
+ apiKey: { service: "vellum", field: "assistant_api_key" },
17
+ assistantId: { service: "vellum", field: "platform_assistant_id" },
18
+ organizationId: { service: "vellum", field: "platform_organization_id" },
19
+ userId: { service: "vellum", field: "platform_user_id" },
20
+ } as const;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Command registration
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export function registerPlatformConnectCommand(platform: Command): void {
27
+ platform
28
+ .command("connect")
29
+ .description(
30
+ "Connect this assistant to the Vellum Platform by storing credentials",
31
+ )
32
+ .addHelpText(
33
+ "after",
34
+ `
35
+ Initiates a platform connection flow. Credentials are collected via a secure
36
+ UI component rendered by the assistant client.
37
+
38
+ Use 'assistant platform status' to check the current connection state and
39
+ 'assistant platform disconnect' to remove stored credentials.
40
+
41
+ Examples:
42
+ $ assistant platform connect
43
+ $ assistant platform connect --json`,
44
+ )
45
+ .action(async (_opts: Record<string, unknown>, cmd: Command) => {
46
+ const jsonMode = shouldOutputJson(cmd);
47
+
48
+ const writeError = (error: string): void => {
49
+ writeOutput(cmd, { ok: false, error });
50
+ process.exitCode = 1;
51
+ };
52
+
53
+ try {
54
+ // Check if already connected
55
+ const existingUrl = await getSecureKeyViaDaemon(
56
+ credentialKey(
57
+ CREDENTIAL_KEYS.baseUrl.service,
58
+ CREDENTIAL_KEYS.baseUrl.field,
59
+ ),
60
+ );
61
+ const existingApiKey = await getSecureKeyViaDaemon(
62
+ credentialKey(
63
+ CREDENTIAL_KEYS.apiKey.service,
64
+ CREDENTIAL_KEYS.apiKey.field,
65
+ ),
66
+ );
67
+
68
+ const alreadyConnected = !!existingUrl && !!existingApiKey;
69
+
70
+ if (alreadyConnected) {
71
+ writeOutput(cmd, {
72
+ ok: true,
73
+ alreadyConnected: true,
74
+ baseUrl: existingUrl,
75
+ });
76
+
77
+ if (!jsonMode) {
78
+ log.info(
79
+ `Already connected to platform at ${existingUrl}. ` +
80
+ `Run 'assistant platform disconnect' first to reconnect.`,
81
+ );
82
+ }
83
+ return;
84
+ }
85
+
86
+ // TODO: Send a UI component to collect credentials from the user
87
+ writeError(
88
+ "Platform connect UI component not yet implemented. " +
89
+ "Credentials will be collected via a secure client-side flow.",
90
+ );
91
+
92
+ if (!jsonMode) {
93
+ log.info(
94
+ "Platform connect will be available once the client-side credential flow is implemented.",
95
+ );
96
+ }
97
+ } catch (err) {
98
+ const message = err instanceof Error ? err.message : String(err);
99
+ writeError(message);
100
+ }
101
+ });
102
+ }
103
+
104
+ export { CREDENTIAL_KEYS };
@@ -0,0 +1,118 @@
1
+ import type { Command } from "commander";
2
+
3
+ import { credentialKey } from "../../../security/credential-key.js";
4
+ import {
5
+ deleteSecureKeyViaDaemon,
6
+ getSecureKeyViaDaemon,
7
+ } from "../../lib/daemon-credential-client.js";
8
+ import { getCliLogger } from "../../logger.js";
9
+ import { shouldOutputJson, writeOutput } from "../../output.js";
10
+ import { CREDENTIAL_KEYS } from "./connect.js";
11
+
12
+ const log = getCliLogger("cli");
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Command registration
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export function registerPlatformDisconnectCommand(platform: Command): void {
19
+ platform
20
+ .command("disconnect")
21
+ .description(
22
+ "Disconnect from the Vellum Platform by removing stored credentials",
23
+ )
24
+ .addHelpText(
25
+ "after",
26
+ `
27
+ Removes all stored platform credentials from the assistant's secure
28
+ credential store. After disconnecting, platform-managed features (managed
29
+ proxy, managed OAuth, callback routing) will no longer be available until
30
+ you reconnect with 'assistant platform connect'.
31
+
32
+ Use 'assistant platform status' to check the current connection state
33
+ before disconnecting.
34
+
35
+ Examples:
36
+ $ assistant platform disconnect
37
+ $ assistant platform disconnect --json`,
38
+ )
39
+ .action(async (_opts: Record<string, unknown>, cmd: Command) => {
40
+ const jsonMode = shouldOutputJson(cmd);
41
+
42
+ const writeError = (error: string): void => {
43
+ writeOutput(cmd, { ok: false, error });
44
+ process.exitCode = 1;
45
+ };
46
+
47
+ try {
48
+ // ---------------------------------------------------------------
49
+ // 1. Check if connected
50
+ // ---------------------------------------------------------------
51
+ const baseUrl = await getSecureKeyViaDaemon(
52
+ credentialKey(
53
+ CREDENTIAL_KEYS.baseUrl.service,
54
+ CREDENTIAL_KEYS.baseUrl.field,
55
+ ),
56
+ );
57
+ const apiKey = await getSecureKeyViaDaemon(
58
+ credentialKey(
59
+ CREDENTIAL_KEYS.apiKey.service,
60
+ CREDENTIAL_KEYS.apiKey.field,
61
+ ),
62
+ );
63
+
64
+ if (!baseUrl && !apiKey) {
65
+ writeError(
66
+ "Not connected to a platform. Nothing to disconnect.\n\n" +
67
+ "Run 'assistant platform status' to check connection state.",
68
+ );
69
+ return;
70
+ }
71
+
72
+ // ---------------------------------------------------------------
73
+ // 2. Delete all platform credentials
74
+ // ---------------------------------------------------------------
75
+ const keysToDelete = [
76
+ CREDENTIAL_KEYS.baseUrl,
77
+ CREDENTIAL_KEYS.apiKey,
78
+ CREDENTIAL_KEYS.assistantId,
79
+ CREDENTIAL_KEYS.organizationId,
80
+ CREDENTIAL_KEYS.userId,
81
+ ] as const;
82
+
83
+ const failedKeys: string[] = [];
84
+ for (const key of keysToDelete) {
85
+ const result = await deleteSecureKeyViaDaemon(
86
+ "credential",
87
+ `${key.service}:${key.field}`,
88
+ );
89
+ if (result === "error") {
90
+ failedKeys.push(`${key.service}:${key.field}`);
91
+ }
92
+ }
93
+
94
+ if (failedKeys.length > 0) {
95
+ writeError(`Failed to delete credentials: ${failedKeys.join(", ")}`);
96
+ return;
97
+ }
98
+
99
+ // ---------------------------------------------------------------
100
+ // 3. Output result
101
+ // ---------------------------------------------------------------
102
+ writeOutput(cmd, {
103
+ ok: true,
104
+ disconnected: true,
105
+ previousBaseUrl: baseUrl ?? null,
106
+ });
107
+
108
+ if (!jsonMode) {
109
+ log.info(
110
+ `Disconnected from platform${baseUrl ? ` at ${baseUrl}` : ""}`,
111
+ );
112
+ }
113
+ } catch (err) {
114
+ const message = err instanceof Error ? err.message : String(err);
115
+ writeError(message);
116
+ }
117
+ });
118
+ }
@@ -3,9 +3,13 @@ import type { Command } from "commander";
3
3
  import {
4
4
  registerCallbackRoute,
5
5
  resolvePlatformCallbackRegistrationContext,
6
- } from "../../inbound/platform-callback-registration.js";
7
- import { log } from "../logger.js";
8
- import { shouldOutputJson, writeOutput } from "../output.js";
6
+ } from "../../../inbound/platform-callback-registration.js";
7
+ import { credentialKey } from "../../../security/credential-key.js";
8
+ import { getSecureKeyViaDaemon } from "../../lib/daemon-credential-client.js";
9
+ import { log } from "../../logger.js";
10
+ import { shouldOutputJson, writeOutput } from "../../output.js";
11
+ import { CREDENTIAL_KEYS, registerPlatformConnectCommand } from "./connect.js";
12
+ import { registerPlatformDisconnectCommand } from "./disconnect.js";
9
13
 
10
14
  export function registerPlatformCommand(program: Command): void {
11
15
  const platform = program
@@ -16,32 +20,42 @@ export function registerPlatformCommand(program: Command): void {
16
20
  platform.addHelpText(
17
21
  "after",
18
22
  `
19
- The platform subsystem manages callback routing, containerized deployment
20
- context, and webhook forwarding for assistants running inside platform
21
- containers. When IS_CONTAINERIZED=true with a configured VELLUM_PLATFORM_URL
23
+ The platform subsystem manages the connection to Vellum Platform, callback
24
+ routing, containerized deployment context, and webhook forwarding for
25
+ assistants. Use 'connect', 'status', and 'disconnect' to manage platform
26
+ credentials. When IS_CONTAINERIZED=true with a configured VELLUM_PLATFORM_URL
22
27
  and PLATFORM_ASSISTANT_ID, external service callbacks (Telegram webhooks,
23
28
  Twilio webhooks, OAuth redirects) route through the platform's gateway proxy
24
29
  instead of hitting the assistant directly.
25
30
 
26
31
  Examples:
27
32
  $ assistant platform status --json
28
- $ assistant platform callback-routes register --path webhooks/telegram --type telegram --json
29
- $ assistant platform status`,
33
+ $ assistant platform connect
34
+ $ assistant platform disconnect
35
+ $ assistant platform callback-routes register --path webhooks/telegram --type telegram --json`,
30
36
  );
31
37
 
32
38
  // ---------------------------------------------------------------------------
33
- // status
39
+ // connect — store platform credentials and validate the connection
40
+ // ---------------------------------------------------------------------------
41
+
42
+ registerPlatformConnectCommand(platform);
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // status — deployment context and connection status combined
34
46
  // ---------------------------------------------------------------------------
35
47
 
36
48
  platform
37
49
  .command("status")
38
- .description("Show current platform deployment context")
50
+ .description(
51
+ "Show current platform deployment context and connection status",
52
+ )
39
53
  .addHelpText(
40
54
  "after",
41
55
  `
42
- Reads platform-related environment variables and returns the current
43
- containerized deployment context. Does not require the assistant to be
44
- running.
56
+ Reads platform-related environment variables and stored credentials to report
57
+ the current containerized deployment context and connection state. Does not
58
+ require the assistant to be running.
45
59
 
46
60
  Fields:
47
61
  containerized Whether IS_CONTAINERIZED is set (boolean)
@@ -51,40 +65,96 @@ Fields:
51
65
  value not disclosed)
52
66
  hasAssistantApiKey Whether a stored assistant API key is available
53
67
  available Whether callback registration prerequisites are satisfied
68
+ connected Whether platform credentials are stored (boolean)
69
+ organizationId The platform organization ID (from stored credentials)
70
+ userId The platform user ID (from stored credentials)
54
71
 
55
72
  Examples:
56
73
  $ assistant platform status
57
74
  $ assistant platform status --json`,
58
75
  )
59
76
  .action(async (_opts: Record<string, unknown>, cmd: Command) => {
60
- const context = await resolvePlatformCallbackRegistrationContext();
61
- const result = {
62
- containerized: context.containerized,
63
- baseUrl: context.platformBaseUrl,
64
- assistantId: context.assistantId,
65
- hasInternalApiKey: context.hasInternalApiKey,
66
- hasAssistantApiKey: context.hasAssistantApiKey,
67
- available: context.enabled,
68
- };
69
-
70
- writeOutput(cmd, result);
71
-
72
- if (!shouldOutputJson(cmd)) {
73
- log.info(`Containerized: ${result.containerized}`);
74
- log.info(`Base URL: ${result.baseUrl || "(not set)"}`);
75
- log.info(`Assistant ID: ${result.assistantId || "(not set)"}`);
76
- log.info(
77
- `Internal API key: ${result.hasInternalApiKey ? "set" : "not set"}`,
78
- );
79
- log.info(
80
- `Assistant API key: ${result.hasAssistantApiKey ? "set" : "not set"}`,
81
- );
82
- log.info(
83
- `Callback registration available: ${result.available ? "yes" : "no"}`,
84
- );
77
+ try {
78
+ const context = await resolvePlatformCallbackRegistrationContext();
79
+
80
+ const storedBaseUrl =
81
+ (await getSecureKeyViaDaemon(
82
+ credentialKey(
83
+ CREDENTIAL_KEYS.baseUrl.service,
84
+ CREDENTIAL_KEYS.baseUrl.field,
85
+ ),
86
+ )) ?? "";
87
+ const hasStoredApiKey = !!(await getSecureKeyViaDaemon(
88
+ credentialKey(
89
+ CREDENTIAL_KEYS.apiKey.service,
90
+ CREDENTIAL_KEYS.apiKey.field,
91
+ ),
92
+ ));
93
+ const organizationId =
94
+ (
95
+ await getSecureKeyViaDaemon(
96
+ credentialKey(
97
+ CREDENTIAL_KEYS.organizationId.service,
98
+ CREDENTIAL_KEYS.organizationId.field,
99
+ ),
100
+ )
101
+ )?.trim() ?? "";
102
+ const userId =
103
+ (
104
+ await getSecureKeyViaDaemon(
105
+ credentialKey(
106
+ CREDENTIAL_KEYS.userId.service,
107
+ CREDENTIAL_KEYS.userId.field,
108
+ ),
109
+ )
110
+ )?.trim() ?? "";
111
+
112
+ const connected = !!storedBaseUrl && hasStoredApiKey;
113
+
114
+ const result = {
115
+ containerized: context.containerized,
116
+ baseUrl: context.platformBaseUrl,
117
+ assistantId: context.assistantId,
118
+ hasInternalApiKey: context.hasInternalApiKey,
119
+ hasAssistantApiKey: context.hasAssistantApiKey,
120
+ available: context.enabled,
121
+ connected,
122
+ organizationId: organizationId || null,
123
+ userId: userId || null,
124
+ };
125
+
126
+ writeOutput(cmd, result);
127
+
128
+ if (!shouldOutputJson(cmd)) {
129
+ log.info(`Containerized: ${result.containerized}`);
130
+ log.info(`Base URL: ${result.baseUrl || "(not set)"}`);
131
+ log.info(`Assistant ID: ${result.assistantId || "(not set)"}`);
132
+ log.info(
133
+ `Internal API key: ${result.hasInternalApiKey ? "set" : "not set"}`,
134
+ );
135
+ log.info(
136
+ `Assistant API key: ${result.hasAssistantApiKey ? "set" : "not set"}`,
137
+ );
138
+ log.info(
139
+ `Callback registration available: ${result.available ? "yes" : "no"}`,
140
+ );
141
+ log.info(`Connected: ${connected}`);
142
+ log.info(`Organization ID: ${organizationId || "(not set)"}`);
143
+ log.info(`User ID: ${userId || "(not set)"}`);
144
+ }
145
+ } catch (err) {
146
+ const message = err instanceof Error ? err.message : String(err);
147
+ writeOutput(cmd, { ok: false, error: message });
148
+ process.exitCode = 1;
85
149
  }
86
150
  });
87
151
 
152
+ // ---------------------------------------------------------------------------
153
+ // disconnect — remove stored platform credentials
154
+ // ---------------------------------------------------------------------------
155
+
156
+ registerPlatformDisconnectCommand(platform);
157
+
88
158
  // ---------------------------------------------------------------------------
89
159
  // callback-routes
90
160
  // ---------------------------------------------------------------------------