@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.
- package/AGENTS.md +9 -1
- package/ARCHITECTURE.md +48 -48
- package/Dockerfile +2 -0
- package/README.md +1 -1
- package/docs/architecture/integrations.md +6 -13
- package/docs/architecture/memory.md +7 -12
- package/docs/architecture/security.md +5 -5
- package/docs/credential-execution-service.md +9 -9
- package/docs/skills.md +1 -1
- package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
- package/openapi.yaml +7130 -0
- package/package.json +2 -1
- package/scripts/generate-openapi.ts +562 -0
- package/src/__tests__/acp-session.test.ts +239 -44
- package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
- package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
- package/src/__tests__/browser-skill-endstate.test.ts +1 -1
- package/src/__tests__/btw-routes.test.ts +8 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
- package/src/__tests__/channel-approvals.test.ts +7 -7
- package/src/__tests__/channel-readiness-service.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +10 -2
- package/src/__tests__/context-memory-e2e.test.ts +2 -6
- package/src/__tests__/conversation-skill-tools.test.ts +1 -3
- package/src/__tests__/conversation-title-service.test.ts +2 -15
- package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
- package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
- package/src/__tests__/credential-security-e2e.test.ts +4 -4
- package/src/__tests__/credential-security-invariants.test.ts +3 -3
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/heartbeat-service.test.ts +35 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
- package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
- package/src/__tests__/log-export-workspace.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
- package/src/__tests__/memory-recall-log-store.test.ts +182 -0
- package/src/__tests__/memory-recall-quality.test.ts +6 -8
- package/src/__tests__/memory-regressions.test.ts +53 -42
- package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
- package/src/__tests__/messaging-skill-split.test.ts +2 -17
- package/src/__tests__/oauth-cli.test.ts +98 -551
- package/src/__tests__/platform-callback-registration.test.ts +119 -0
- package/src/__tests__/secret-ingress-channel.test.ts +261 -0
- package/src/__tests__/secret-ingress-cli.test.ts +201 -0
- package/src/__tests__/secret-ingress-http.test.ts +312 -0
- package/src/__tests__/secret-ingress.test.ts +283 -0
- package/src/__tests__/secret-onetime-send.test.ts +4 -4
- package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
- package/src/__tests__/skill-feature-flags.test.ts +11 -19
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
- package/src/__tests__/skill-load-inline-command.test.ts +3 -3
- package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
- package/src/__tests__/skill-memory.test.ts +2 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
- package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
- package/src/__tests__/skills.test.ts +16 -2
- package/src/__tests__/slack-channel-config.test.ts +1 -1
- package/src/__tests__/slack-skill.test.ts +5 -69
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
- package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
- package/src/acp/client-handler.ts +113 -31
- package/src/acp/session-manager.ts +29 -27
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/cli/AGENTS.md +73 -0
- package/src/cli/commands/autonomy.ts +3 -5
- package/src/cli/commands/credential-execution.ts +1 -2
- package/src/cli/commands/credentials.ts +4 -4
- package/src/cli/commands/memory.ts +2 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
- package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
- package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
- package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
- package/src/cli/commands/oauth/apps.ts +29 -11
- package/src/cli/commands/oauth/connect.ts +373 -0
- package/src/cli/commands/oauth/connections.ts +14 -493
- package/src/cli/commands/oauth/disconnect.ts +333 -0
- package/src/cli/commands/oauth/index.ts +62 -10
- package/src/cli/commands/oauth/mode.ts +263 -0
- package/src/cli/commands/oauth/ping.ts +222 -0
- package/src/cli/commands/oauth/providers.ts +30 -3
- package/src/cli/commands/oauth/request.ts +576 -0
- package/src/cli/commands/oauth/shared.ts +132 -0
- package/src/cli/commands/oauth/status.ts +202 -0
- package/src/cli/commands/oauth/token.ts +159 -0
- package/src/cli/commands/platform.ts +20 -14
- package/src/cli.ts +82 -17
- package/src/config/assistant-feature-flags.ts +74 -11
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +13 -36
- package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/SKILL.md +2 -2
- package/src/config/bundled-skills/settings/SKILL.md +5 -3
- package/src/config/bundled-skills/settings/TOOLS.json +17 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
- package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
- package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
- package/src/config/bundled-skills/slack/SKILL.md +58 -44
- package/src/config/bundled-tool-registry.ts +2 -19
- package/src/config/env.ts +5 -1
- package/src/config/feature-flag-registry.json +57 -41
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/security.ts +9 -1
- package/src/config/schemas/services.ts +1 -1
- package/src/config/skill-state.ts +1 -3
- package/src/config/skills.ts +2 -4
- package/src/credential-execution/feature-gates.ts +9 -16
- package/src/credential-execution/process-manager.ts +12 -0
- package/src/daemon/config-watcher.ts +4 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
- package/src/daemon/conversation-agent-loop.ts +49 -2
- package/src/daemon/conversation-memory.ts +0 -1
- package/src/daemon/handlers/config-slack-channel.ts +43 -1
- package/src/daemon/handlers/conversations.ts +41 -33
- package/src/daemon/lifecycle.ts +28 -5
- package/src/daemon/message-types/acp.ts +0 -15
- package/src/daemon/message-types/memory.ts +0 -1
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +9 -0
- package/src/daemon/server.ts +19 -7
- package/src/email/feature-gate.ts +3 -3
- package/src/heartbeat/heartbeat-service.ts +48 -0
- package/src/inbound/platform-callback-registration.ts +61 -7
- package/src/mcp/mcp-oauth-provider.ts +3 -3
- package/src/memory/app-store.ts +3 -3
- package/src/memory/conversation-crud.ts +124 -0
- package/src/memory/conversation-title-service.ts +7 -17
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-local.ts +47 -2
- package/src/memory/indexer.ts +13 -10
- package/src/memory/items-extractor.ts +12 -4
- package/src/memory/job-utils.ts +5 -0
- package/src/memory/jobs-store.ts +10 -2
- package/src/memory/journal-memory.ts +6 -2
- package/src/memory/llm-request-log-store.ts +88 -21
- package/src/memory/memory-recall-log-store.ts +128 -0
- package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
- package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/validate-migration-state.ts +14 -1
- package/src/memory/retriever.test.ts +4 -5
- package/src/memory/schema/infrastructure.ts +31 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +54 -0
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +26 -5
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -2
- package/src/permissions/trust-client.ts +2 -2
- package/src/platform/client.ts +2 -2
- package/src/prompts/journal-context.ts +6 -1
- package/src/providers/anthropic/client.ts +143 -1
- package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
- package/src/runtime/auth/route-policy.ts +0 -1
- package/src/runtime/btw-sidechain.ts +7 -1
- package/src/runtime/channel-approvals.ts +2 -2
- package/src/runtime/channel-readiness-service.ts +30 -7
- package/src/runtime/http-router.ts +31 -0
- package/src/runtime/http-server.ts +21 -4
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/pending-interactions.ts +21 -3
- package/src/runtime/routes/acp-routes.ts +46 -28
- package/src/runtime/routes/app-management-routes.ts +123 -0
- package/src/runtime/routes/app-routes.ts +31 -0
- package/src/runtime/routes/approval-routes.ts +108 -3
- package/src/runtime/routes/attachment-routes.ts +45 -0
- package/src/runtime/routes/avatar-routes.ts +16 -0
- package/src/runtime/routes/brain-graph-routes.ts +18 -0
- package/src/runtime/routes/btw-routes.ts +20 -0
- package/src/runtime/routes/call-routes.ts +81 -0
- package/src/runtime/routes/channel-readiness-routes.ts +48 -7
- package/src/runtime/routes/channel-routes.ts +18 -0
- package/src/runtime/routes/channel-verification-routes.ts +49 -1
- package/src/runtime/routes/contact-routes.ts +77 -0
- package/src/runtime/routes/conversation-attention-routes.ts +37 -0
- package/src/runtime/routes/conversation-management-routes.ts +94 -0
- package/src/runtime/routes/conversation-query-routes.ts +78 -0
- package/src/runtime/routes/conversation-routes.ts +115 -38
- package/src/runtime/routes/conversation-starter-routes.ts +29 -0
- package/src/runtime/routes/debug-routes.ts +23 -0
- package/src/runtime/routes/diagnostics-routes.ts +30 -0
- package/src/runtime/routes/documents-routes.ts +42 -0
- package/src/runtime/routes/events-routes.ts +10 -0
- package/src/runtime/routes/global-search-routes.ts +35 -0
- package/src/runtime/routes/guardian-action-routes.ts +47 -2
- package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
- package/src/runtime/routes/heartbeat-routes.ts +278 -0
- package/src/runtime/routes/host-bash-routes.ts +16 -1
- package/src/runtime/routes/host-cu-routes.ts +23 -1
- package/src/runtime/routes/host-file-routes.ts +18 -1
- package/src/runtime/routes/identity-routes.ts +35 -0
- package/src/runtime/routes/inbound-message-handler.ts +46 -25
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
- package/src/runtime/routes/integrations/twilio.ts +32 -22
- package/src/runtime/routes/invite-routes.ts +83 -0
- package/src/runtime/routes/log-export-routes.ts +14 -0
- package/src/runtime/routes/memory-item-routes.ts +99 -1
- package/src/runtime/routes/migration-rollback-routes.ts +25 -0
- package/src/runtime/routes/migration-routes.ts +40 -0
- package/src/runtime/routes/notification-routes.ts +20 -0
- package/src/runtime/routes/oauth-apps.ts +11 -3
- package/src/runtime/routes/pairing-routes.ts +15 -0
- package/src/runtime/routes/recording-routes.ts +72 -0
- package/src/runtime/routes/schedule-routes.ts +77 -5
- package/src/runtime/routes/secret-routes.ts +63 -1
- package/src/runtime/routes/settings-routes.ts +91 -1
- package/src/runtime/routes/skills-routes.ts +98 -16
- package/src/runtime/routes/subagents-routes.ts +38 -3
- package/src/runtime/routes/surface-action-routes.ts +66 -24
- package/src/runtime/routes/surface-content-routes.ts +20 -0
- package/src/runtime/routes/telemetry-routes.ts +12 -0
- package/src/runtime/routes/trace-event-routes.ts +25 -0
- package/src/runtime/routes/trust-rules-routes.ts +46 -0
- package/src/runtime/routes/tts-routes.ts +15 -4
- package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
- package/src/runtime/routes/usage-routes.ts +59 -0
- package/src/runtime/routes/watch-routes.ts +28 -0
- package/src/runtime/routes/work-items-routes.ts +59 -0
- package/src/runtime/routes/workspace-commit-routes.ts +12 -0
- package/src/runtime/routes/workspace-routes.ts +102 -0
- package/src/schedule/scheduler.ts +7 -1
- package/src/security/AGENTS.md +7 -0
- package/src/security/credential-backend.ts +1 -1
- package/src/security/encrypted-store.ts +3 -3
- package/src/security/oauth2.ts +55 -0
- package/src/security/secret-ingress.ts +174 -0
- package/src/security/secret-patterns.ts +133 -0
- package/src/security/secret-scanner.ts +28 -117
- package/src/signals/confirm.ts +12 -8
- package/src/signals/user-message.ts +18 -3
- package/src/skills/skill-memory.ts +1 -2
- package/src/tasks/task-runner.ts +7 -1
- package/src/tools/credentials/broker.ts +1 -1
- package/src/tools/credentials/metadata-store.ts +1 -1
- package/src/tools/credentials/vault.ts +2 -3
- package/src/tools/memory/definitions.ts +1 -1
- package/src/tools/memory/handlers.test.ts +2 -4
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/tool-manifest.ts +1 -1
- package/src/util/log-redact.ts +9 -34
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
- package/src/workspace/migrations/AGENTS.md +11 -0
- package/src/workspace/migrations/runner.ts +16 -6
- package/src/workspace/migrations/types.ts +7 -0
- package/docs/architecture/keychain-broker.md +0 -69
- package/src/__tests__/keychain-broker-client.test.ts +0 -800
- package/src/cli/commands/oauth/platform.ts +0 -525
- package/src/config/bundled-skills/slack/TOOLS.json +0 -272
- package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
- 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(
|
|
102
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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;
|