@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.
- package/Dockerfile +42 -9
- package/docs/architecture/integrations.md +34 -32
- package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
- package/openapi.yaml +87 -9
- package/package.json +1 -1
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/credential-security-invariants.test.ts +9 -15
- package/src/__tests__/credential-vault-unit.test.ts +32 -34
- package/src/__tests__/credential-vault.test.ts +25 -33
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/daemon-credential-client.test.ts +2 -2
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/host-bash-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-proxy.test.ts +90 -0
- package/src/__tests__/host-file-proxy.test.ts +89 -0
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +126 -119
- package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/cli/AGENTS.md +47 -7
- package/src/cli/commands/browser-relay.ts +2 -17
- package/src/cli/commands/contacts.ts +6 -4
- package/src/cli/commands/conversations.ts +13 -1
- package/src/cli/commands/credential-execution.ts +16 -1
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
- package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
- package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
- package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
- package/src/cli/commands/oauth/apps.ts +63 -44
- package/src/cli/commands/oauth/connect.ts +187 -155
- package/src/cli/commands/oauth/disconnect.ts +27 -75
- package/src/cli/commands/oauth/index.ts +36 -46
- package/src/cli/commands/oauth/mode.ts +22 -34
- package/src/cli/commands/oauth/ping.ts +19 -45
- package/src/cli/commands/oauth/providers.ts +569 -62
- package/src/cli/commands/oauth/request.ts +36 -48
- package/src/cli/commands/oauth/shared.ts +1 -19
- package/src/cli/commands/oauth/status.ts +14 -25
- package/src/cli/commands/oauth/token.ts +25 -34
- package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- package/src/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
- package/src/cli/commands/sequence.ts +5 -4
- package/src/cli/commands/shotgun.ts +16 -0
- package/src/cli/commands/skills.ts +173 -41
- package/src/cli/commands/usage.ts +5 -11
- package/src/cli/lib/daemon-credential-client.ts +22 -38
- package/src/cli/program.ts +1 -1
- package/src/config/assistant-feature-flags.ts +3 -7
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/conversations/SKILL.md +20 -0
- package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
- package/src/config/bundled-skills/gmail/SKILL.md +13 -13
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
- package/src/config/bundled-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +2 -2
- package/src/credential-execution/client.ts +15 -3
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-messaging.ts +9 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -0
- package/src/daemon/conversation-surfaces.ts +120 -14
- package/src/daemon/conversation.ts +5 -0
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/handlers/skills.ts +148 -3
- package/src/daemon/host-bash-proxy.ts +16 -0
- package/src/daemon/host-cu-proxy.ts +16 -0
- package/src/daemon/host-file-proxy.ts +16 -0
- package/src/daemon/lifecycle.ts +56 -5
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/message-types/guardian-actions.ts +2 -0
- package/src/daemon/message-types/host-bash.ts +6 -1
- package/src/daemon/message-types/host-cu.ts +6 -1
- package/src/daemon/message-types/host-file.ts +6 -1
- package/src/daemon/message-types/integrations.ts +0 -1
- package/src/daemon/server.ts +29 -2
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +7 -12
- package/src/index.ts +0 -12
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/memory/conversation-crud.ts +92 -3
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/db-init.ts +16 -0
- package/src/memory/journal-memory.ts +8 -2
- package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
- package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
- package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/oauth.ts +11 -0
- package/src/messaging/provider.ts +13 -12
- package/src/messaging/providers/gmail/adapter.ts +44 -35
- package/src/messaging/providers/slack/adapter.ts +63 -33
- package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
- package/src/messaging/providers/whatsapp/adapter.ts +6 -8
- package/src/notifications/adapters/telegram.ts +78 -2
- package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
- package/src/oauth/byo-connection.test.ts +22 -24
- package/src/oauth/connect-orchestrator.ts +37 -76
- package/src/oauth/connect-types.ts +7 -65
- package/src/oauth/connection-resolver.test.ts +13 -13
- package/src/oauth/connection-resolver.ts +3 -4
- package/src/oauth/identity-verifier.ts +177 -0
- package/src/oauth/oauth-store.ts +228 -3
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +247 -34
- package/src/permissions/checker.ts +127 -1
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +54 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +2 -33
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-server.ts +12 -18
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +31 -0
- package/src/runtime/routes/conversation-routes.ts +79 -4
- package/src/runtime/routes/guardian-action-routes.ts +15 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/oauth-apps.ts +2 -1
- package/src/runtime/routes/secret-routes.ts +45 -15
- package/src/runtime/routes/settings-routes.ts +12 -19
- package/src/runtime/routes/skills-routes.ts +45 -4
- package/src/schedule/integration-status.ts +2 -2
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +3 -17
- package/src/security/secure-keys.ts +207 -7
- package/src/security/token-manager.ts +3 -6
- package/src/signals/bash.ts +6 -1
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +34 -45
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/shell.ts +16 -3
- package/src/util/logger.ts +11 -1
- package/src/util/platform.ts +1 -91
- package/src/util/sentry-log-stream.ts +51 -0
- package/src/watcher/providers/github.ts +2 -2
- package/src/watcher/providers/gmail.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +1 -1
- package/src/watcher/providers/linear.ts +2 -2
- package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
- package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/cli/commands/oauth/connections.ts +0 -255
- 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 "
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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
|
|
20
|
-
context, and webhook forwarding for
|
|
21
|
-
|
|
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
|
|
29
|
-
$ assistant platform
|
|
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
|
-
//
|
|
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(
|
|
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
|
|
43
|
-
containerized deployment context. Does not
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|