@vellumai/assistant 0.5.11 → 0.5.12
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 +1 -0
- 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/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__/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__/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__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -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__/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/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-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +1 -1
- package/src/credential-execution/client.ts +1 -1
- 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/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 +47 -1
- 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/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/db-init.ts +16 -0
- 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/system-prompt.ts +43 -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 +5 -3
- package/src/runtime/http-types.ts +8 -1
- 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 +36 -13
- 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/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/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
|
@@ -106,7 +106,7 @@ async function queueApiKeyPropagation(
|
|
|
106
106
|
|
|
107
107
|
export async function handleAddSecret(
|
|
108
108
|
req: Request,
|
|
109
|
-
|
|
109
|
+
deps?: SecretRouteDeps,
|
|
110
110
|
): Promise<Response> {
|
|
111
111
|
let body: { type?: string; name?: string; value?: string };
|
|
112
112
|
try {
|
|
@@ -192,9 +192,7 @@ export async function handleAddSecret(
|
|
|
192
192
|
500,
|
|
193
193
|
);
|
|
194
194
|
}
|
|
195
|
-
|
|
196
|
-
invalidateConfigCache();
|
|
197
|
-
await initializeProviders(getConfig());
|
|
195
|
+
await refreshProvidersAfterSecretChange(deps);
|
|
198
196
|
log.info({ provider: name }, "API key updated via HTTP");
|
|
199
197
|
return Response.json({ success: true, type, name }, { status: 201 });
|
|
200
198
|
}
|
|
@@ -275,12 +273,12 @@ export async function handleAddSecret(
|
|
|
275
273
|
}
|
|
276
274
|
}
|
|
277
275
|
if (isManagedProxyCredential(service, field)) {
|
|
278
|
-
await
|
|
276
|
+
await refreshProvidersAfterSecretChange(deps);
|
|
279
277
|
if (service === "vellum" && field === "assistant_api_key") {
|
|
280
278
|
// Push the API key to CES so managed credential materialization
|
|
281
279
|
// works even though the handshake ran before the key was available.
|
|
282
280
|
const generation = ++apiKeyGeneration;
|
|
283
|
-
const cesClient = getCesClient?.();
|
|
281
|
+
const cesClient = deps?.getCesClient?.();
|
|
284
282
|
if (cesClient) {
|
|
285
283
|
if (cesClient.isReady()) {
|
|
286
284
|
try {
|
|
@@ -394,7 +392,10 @@ export async function handleReadSecret(req: Request): Promise<Response> {
|
|
|
394
392
|
}
|
|
395
393
|
}
|
|
396
394
|
|
|
397
|
-
export async function handleDeleteSecret(
|
|
395
|
+
export async function handleDeleteSecret(
|
|
396
|
+
req: Request,
|
|
397
|
+
deps?: SecretRouteDeps,
|
|
398
|
+
): Promise<Response> {
|
|
398
399
|
let body: { type?: string; name?: string };
|
|
399
400
|
try {
|
|
400
401
|
body = (await req.json()) as { type?: string; name?: string };
|
|
@@ -438,9 +439,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
438
439
|
500,
|
|
439
440
|
);
|
|
440
441
|
}
|
|
441
|
-
|
|
442
|
-
invalidateConfigCache();
|
|
443
|
-
await initializeProviders(getConfig());
|
|
442
|
+
await refreshProvidersAfterSecretChange(deps);
|
|
444
443
|
log.info({ provider: name }, "API key deleted via HTTP");
|
|
445
444
|
return Response.json({ success: true, type, name });
|
|
446
445
|
}
|
|
@@ -488,7 +487,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
488
487
|
setSentryUserId(undefined);
|
|
489
488
|
}
|
|
490
489
|
if (isManagedProxyCredential(service, field)) {
|
|
491
|
-
await
|
|
490
|
+
await refreshProvidersAfterSecretChange(deps);
|
|
492
491
|
}
|
|
493
492
|
log.info({ service, field }, "Credential deleted via HTTP");
|
|
494
493
|
return Response.json({ success: true, type, name });
|
|
@@ -548,6 +547,30 @@ export async function handleListSecrets(): Promise<Response> {
|
|
|
548
547
|
export interface SecretRouteDeps {
|
|
549
548
|
/** Accessor for the CES client, used to push API key updates after hatch. */
|
|
550
549
|
getCesClient?: () => CesClient | undefined;
|
|
550
|
+
/**
|
|
551
|
+
* Called after provider-affecting credentials change so live conversations
|
|
552
|
+
* can be reloaded with fresh provider instances.
|
|
553
|
+
*/
|
|
554
|
+
onProviderCredentialsChanged?: () => void | Promise<void>;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function refreshProvidersAfterSecretChange(
|
|
558
|
+
deps?: SecretRouteDeps,
|
|
559
|
+
): Promise<void> {
|
|
560
|
+
clearEmbeddingBackendCache();
|
|
561
|
+
invalidateConfigCache();
|
|
562
|
+
await initializeProviders(getConfig());
|
|
563
|
+
|
|
564
|
+
if (!deps?.onProviderCredentialsChanged) return;
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
await deps.onProviderCredentialsChanged();
|
|
568
|
+
} catch (err) {
|
|
569
|
+
log.warn(
|
|
570
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
571
|
+
"Failed to refresh live conversations after provider credential change",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
551
574
|
}
|
|
552
575
|
|
|
553
576
|
export function secretRouteDefinitions(
|
|
@@ -557,7 +580,7 @@ export function secretRouteDefinitions(
|
|
|
557
580
|
{
|
|
558
581
|
endpoint: "secrets",
|
|
559
582
|
method: "POST",
|
|
560
|
-
handler: async ({ req }) => handleAddSecret(req, deps
|
|
583
|
+
handler: async ({ req }) => handleAddSecret(req, deps),
|
|
561
584
|
summary: "Add a secret",
|
|
562
585
|
description:
|
|
563
586
|
"Store a new secret (API key, OAuth token, etc.) in the credential vault.",
|
|
@@ -576,7 +599,7 @@ export function secretRouteDefinitions(
|
|
|
576
599
|
{
|
|
577
600
|
endpoint: "secrets",
|
|
578
601
|
method: "DELETE",
|
|
579
|
-
handler: async ({ req }) => handleDeleteSecret(req),
|
|
602
|
+
handler: async ({ req }) => handleDeleteSecret(req, deps),
|
|
580
603
|
summary: "Delete a secret",
|
|
581
604
|
description: "Remove a secret from the credential vault by name.",
|
|
582
605
|
tags: ["secrets"],
|
|
@@ -30,10 +30,6 @@ import {
|
|
|
30
30
|
getMostRecentAppByProvider,
|
|
31
31
|
getProvider,
|
|
32
32
|
} from "../../oauth/oauth-store.js";
|
|
33
|
-
import {
|
|
34
|
-
getProviderBehavior,
|
|
35
|
-
resolveService,
|
|
36
|
-
} from "../../oauth/provider-behaviors.js";
|
|
37
33
|
import {
|
|
38
34
|
check,
|
|
39
35
|
classifyRisk,
|
|
@@ -170,14 +166,14 @@ async function handleOAuthConnectStart(body: {
|
|
|
170
166
|
return httpError("BAD_REQUEST", "Missing required field: service", 400);
|
|
171
167
|
}
|
|
172
168
|
|
|
173
|
-
const
|
|
169
|
+
const service = body.service;
|
|
174
170
|
|
|
175
171
|
// Resolve client_id and client_secret from oauth-store.
|
|
176
172
|
let clientId: string | undefined;
|
|
177
173
|
let clientSecret: string | undefined;
|
|
178
174
|
|
|
179
175
|
// Try existing connection first (re-auth flow)
|
|
180
|
-
const conn = getConnectionByProvider(
|
|
176
|
+
const conn = getConnectionByProvider(service);
|
|
181
177
|
if (conn) {
|
|
182
178
|
const app = getApp(conn.oauthAppId);
|
|
183
179
|
if (app) {
|
|
@@ -188,7 +184,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
188
184
|
|
|
189
185
|
// Fall back to most recent app for this provider (first-time connect with stored app)
|
|
190
186
|
if (!clientId) {
|
|
191
|
-
const dbApp = getMostRecentAppByProvider(
|
|
187
|
+
const dbApp = getMostRecentAppByProvider(service);
|
|
192
188
|
if (dbApp) {
|
|
193
189
|
clientId = dbApp.clientId;
|
|
194
190
|
if (!clientSecret) {
|
|
@@ -202,20 +198,17 @@ async function handleOAuthConnectStart(body: {
|
|
|
202
198
|
if (!clientId) {
|
|
203
199
|
return httpError(
|
|
204
200
|
"BAD_REQUEST",
|
|
205
|
-
`No client_id found for "${
|
|
201
|
+
`No client_id found for "${service}". Store it first via the credential vault.`,
|
|
206
202
|
400,
|
|
207
203
|
);
|
|
208
204
|
}
|
|
209
205
|
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const requiresSecret =
|
|
213
|
-
behavior?.setup?.requiresClientSecret ??
|
|
214
|
-
!!(providerRow?.tokenEndpointAuthMethod || providerRow?.extraParams);
|
|
206
|
+
const providerRow = getProvider(service);
|
|
207
|
+
const requiresSecret = !!providerRow?.requiresClientSecret;
|
|
215
208
|
if (requiresSecret && !clientSecret) {
|
|
216
209
|
return httpError(
|
|
217
210
|
"BAD_REQUEST",
|
|
218
|
-
`client_secret is required for "${
|
|
211
|
+
`client_secret is required for "${service}" but not found in the credential store. Store it first via the credential vault.`,
|
|
219
212
|
400,
|
|
220
213
|
);
|
|
221
214
|
}
|
|
@@ -226,7 +219,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
226
219
|
let authUrl: string | undefined;
|
|
227
220
|
|
|
228
221
|
const result = await orchestrateOAuthConnect({
|
|
229
|
-
service
|
|
222
|
+
service,
|
|
230
223
|
requestedScopes: body.requestedScopes,
|
|
231
224
|
clientId,
|
|
232
225
|
clientSecret,
|
|
@@ -238,7 +231,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
238
231
|
// Prefer accountInfo from oauth-store when available.
|
|
239
232
|
let accountInfo = deferredResult.accountInfo;
|
|
240
233
|
try {
|
|
241
|
-
const conn = getConnectionByProvider(
|
|
234
|
+
const conn = getConnectionByProvider(service);
|
|
242
235
|
if (conn?.accountInfo) accountInfo = conn.accountInfo;
|
|
243
236
|
} catch {
|
|
244
237
|
// DB not ready — use orchestrator value
|
|
@@ -277,7 +270,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
277
270
|
|
|
278
271
|
if (!result.success) {
|
|
279
272
|
log.error(
|
|
280
|
-
{ err: result.error, service
|
|
273
|
+
{ err: result.error, service },
|
|
281
274
|
"OAuth connect orchestrator returned error",
|
|
282
275
|
);
|
|
283
276
|
return httpError(
|
|
@@ -298,7 +291,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
298
291
|
// Prefer accountInfo from oauth-store when available.
|
|
299
292
|
let responseAccountInfo = result.accountInfo;
|
|
300
293
|
try {
|
|
301
|
-
const conn = getConnectionByProvider(
|
|
294
|
+
const conn = getConnectionByProvider(service);
|
|
302
295
|
if (conn?.accountInfo) responseAccountInfo = conn.accountInfo;
|
|
303
296
|
} catch {
|
|
304
297
|
// DB not ready — use orchestrator value
|
|
@@ -312,7 +305,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
312
305
|
});
|
|
313
306
|
} catch (err) {
|
|
314
307
|
const message = err instanceof Error ? err.message : String(err);
|
|
315
|
-
log.error({ err, service
|
|
308
|
+
log.error({ err, service }, "OAuth connect flow failed");
|
|
316
309
|
return httpError("INTERNAL_ERROR", sanitizeOAuthError(message), 500);
|
|
317
310
|
}
|
|
318
311
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
inspectSkill,
|
|
24
24
|
installSkill,
|
|
25
25
|
listSkills,
|
|
26
|
+
listSkillsWithCatalog,
|
|
26
27
|
searchSkills,
|
|
27
28
|
uninstallSkill,
|
|
28
29
|
updateSkill,
|
|
@@ -47,13 +48,53 @@ export function skillRouteDefinitions(deps: SkillRouteDeps): RouteDefinition[] {
|
|
|
47
48
|
method: "GET",
|
|
48
49
|
policyKey: "skills",
|
|
49
50
|
summary: "List all skills",
|
|
50
|
-
description:
|
|
51
|
+
description:
|
|
52
|
+
"Return all installed skills. Pass ?include=catalog to also include available catalog skills.",
|
|
51
53
|
tags: ["skills"],
|
|
54
|
+
queryParams: [
|
|
55
|
+
{
|
|
56
|
+
name: "include",
|
|
57
|
+
schema: { type: "string", enum: ["catalog"] },
|
|
58
|
+
description:
|
|
59
|
+
"Optional inclusion flag. Use 'catalog' to merge available Vellum catalog skills into the response.",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
52
62
|
responseBody: z.object({
|
|
53
|
-
skills: z
|
|
63
|
+
skills: z
|
|
64
|
+
.array(
|
|
65
|
+
z.object({
|
|
66
|
+
id: z.string(),
|
|
67
|
+
name: z.string(),
|
|
68
|
+
description: z.string(),
|
|
69
|
+
emoji: z.string().optional(),
|
|
70
|
+
homepage: z.string().optional(),
|
|
71
|
+
source: z.enum([
|
|
72
|
+
"bundled",
|
|
73
|
+
"managed",
|
|
74
|
+
"workspace",
|
|
75
|
+
"clawhub",
|
|
76
|
+
"extra",
|
|
77
|
+
"catalog",
|
|
78
|
+
]),
|
|
79
|
+
state: z.enum(["enabled", "disabled"]),
|
|
80
|
+
installStatus: z.enum(["bundled", "installed", "available"]),
|
|
81
|
+
updateAvailable: z.boolean(),
|
|
82
|
+
provenance: z.object({
|
|
83
|
+
kind: z.enum(["first-party", "third-party", "local"]),
|
|
84
|
+
provider: z.string().optional(),
|
|
85
|
+
originId: z.string().optional(),
|
|
86
|
+
sourceUrl: z.string().optional(),
|
|
87
|
+
}),
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
.describe("Skill objects"),
|
|
54
91
|
}),
|
|
55
|
-
handler: () => {
|
|
56
|
-
const
|
|
92
|
+
handler: async ({ url }) => {
|
|
93
|
+
const include = url.searchParams.get("include");
|
|
94
|
+
const skills =
|
|
95
|
+
include === "catalog"
|
|
96
|
+
? await listSkillsWithCatalog(ctx())
|
|
97
|
+
: listSkills(ctx());
|
|
57
98
|
return Response.json({ skills });
|
|
58
99
|
},
|
|
59
100
|
},
|
|
@@ -15,12 +15,12 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
15
15
|
{
|
|
16
16
|
name: "Gmail",
|
|
17
17
|
category: "email",
|
|
18
|
-
isConnected: () => isProviderConnected("
|
|
18
|
+
isConnected: () => isProviderConnected("google"),
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
name: "Slack",
|
|
22
22
|
category: "messaging",
|
|
23
|
-
isConnected: () => isProviderConnected("
|
|
23
|
+
isConnected: () => isProviderConnected("slack"),
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: "Twilio",
|
|
@@ -30,11 +30,13 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async get(account: string): Promise<CredentialGetResult> {
|
|
33
|
+
if (!this.isAvailable()) {
|
|
34
|
+
return { value: undefined, unreachable: true };
|
|
35
|
+
}
|
|
33
36
|
try {
|
|
34
|
-
const result = await this.client.call(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
37
|
+
const result = await this.client.call(CesRpcMethod.GetCredential, {
|
|
38
|
+
account,
|
|
39
|
+
});
|
|
38
40
|
return {
|
|
39
41
|
value: result.found ? result.value : undefined,
|
|
40
42
|
unreachable: false,
|
|
@@ -46,11 +48,12 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
async set(account: string, value: string): Promise<boolean> {
|
|
51
|
+
if (!this.isAvailable()) return false;
|
|
49
52
|
try {
|
|
50
|
-
const result = await this.client.call(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
53
|
+
const result = await this.client.call(CesRpcMethod.SetCredential, {
|
|
54
|
+
account,
|
|
55
|
+
value,
|
|
56
|
+
});
|
|
54
57
|
return result.ok;
|
|
55
58
|
} catch (err) {
|
|
56
59
|
log.warn({ err, account }, "CES RPC credential set failed");
|
|
@@ -59,11 +62,11 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
async delete(account: string): Promise<DeleteResult> {
|
|
65
|
+
if (!this.isAvailable()) return "error";
|
|
62
66
|
try {
|
|
63
|
-
const result = await this.client.call(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
+
const result = await this.client.call(CesRpcMethod.DeleteCredential, {
|
|
68
|
+
account,
|
|
69
|
+
});
|
|
67
70
|
return result.result;
|
|
68
71
|
} catch (err) {
|
|
69
72
|
log.warn({ err, account }, "CES RPC credential delete failed");
|
|
@@ -72,11 +75,11 @@ export class CesRpcCredentialBackend implements CredentialBackend {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
async list(): Promise<CredentialListResult> {
|
|
78
|
+
if (!this.isAvailable()) {
|
|
79
|
+
return { accounts: [], unreachable: true };
|
|
80
|
+
}
|
|
75
81
|
try {
|
|
76
|
-
const result = await this.client.call(
|
|
77
|
-
CesRpcMethod.ListCredentials,
|
|
78
|
-
{},
|
|
79
|
-
);
|
|
82
|
+
const result = await this.client.call(CesRpcMethod.ListCredentials, {});
|
|
80
83
|
return { accounts: result.accounts, unreachable: false };
|
|
81
84
|
} catch (err) {
|
|
82
85
|
log.warn({ err }, "CES RPC credential list failed");
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTML rendering for OAuth completion pages shown in the browser
|
|
3
|
+
* after a loopback/redirect OAuth flow completes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function escapeHtml(s: string): string {
|
|
7
|
+
return s
|
|
8
|
+
.replace(/&/g, "&")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">")
|
|
11
|
+
.replace(/"/g, """)
|
|
12
|
+
.replace(/'/g, "'");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatProviderName(provider: string): string {
|
|
16
|
+
// Capitalize first letter of each word, handle common acronyms
|
|
17
|
+
return provider
|
|
18
|
+
.split(/[-_]/)
|
|
19
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
20
|
+
.join(" ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderOAuthCompletionPage(
|
|
24
|
+
message: string,
|
|
25
|
+
success: boolean,
|
|
26
|
+
provider?: string,
|
|
27
|
+
): string {
|
|
28
|
+
const displayProvider = provider ? formatProviderName(provider) : "";
|
|
29
|
+
const title = success
|
|
30
|
+
? displayProvider
|
|
31
|
+
? `Connected to ${escapeHtml(displayProvider)}`
|
|
32
|
+
: "Authorization Successful"
|
|
33
|
+
: "Authorization Failed";
|
|
34
|
+
const subtitle = success
|
|
35
|
+
? "You can close this tab and return to your assistant."
|
|
36
|
+
: escapeHtml(message);
|
|
37
|
+
|
|
38
|
+
const checkmarkSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
39
|
+
<circle cx="28" cy="28" r="28" fill="var(--positive-bg)"/>
|
|
40
|
+
<path class="check" d="M17 28.5L24.5 36L39 21" stroke="var(--positive-fg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
41
|
+
</svg>`;
|
|
42
|
+
|
|
43
|
+
const errorSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
44
|
+
<circle cx="28" cy="28" r="28" fill="var(--negative-bg)"/>
|
|
45
|
+
<path class="cross cross-1" d="M20 20L36 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
46
|
+
<path class="cross cross-2" d="M36 20L20 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
47
|
+
</svg>`;
|
|
48
|
+
|
|
49
|
+
return `<!DOCTYPE html>
|
|
50
|
+
<html lang="en">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="utf-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
54
|
+
<title>${escapeHtml(title)}</title>
|
|
55
|
+
<style>
|
|
56
|
+
:root {
|
|
57
|
+
--surface: #F5F3EB;
|
|
58
|
+
--surface-card: #FFFFFF;
|
|
59
|
+
--card-border: #E8E6DA;
|
|
60
|
+
--text-primary: #2A2A28;
|
|
61
|
+
--text-secondary: #4A4A46;
|
|
62
|
+
--text-tertiary: #A1A096;
|
|
63
|
+
--positive-bg: #D4DFD0;
|
|
64
|
+
--positive-fg: #516748;
|
|
65
|
+
--negative-bg: #F7DAC9;
|
|
66
|
+
--negative-fg: #DA491A;
|
|
67
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06);
|
|
68
|
+
--font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
69
|
+
}
|
|
70
|
+
@media (prefers-color-scheme: dark) {
|
|
71
|
+
:root {
|
|
72
|
+
--surface: #1A1A18;
|
|
73
|
+
--surface-card: #2A2A28;
|
|
74
|
+
--card-border: #3A3A37;
|
|
75
|
+
--text-primary: #F5F3EB;
|
|
76
|
+
--text-secondary: #BDB9A9;
|
|
77
|
+
--text-tertiary: #6B6B65;
|
|
78
|
+
--positive-bg: #1A2316;
|
|
79
|
+
--positive-fg: #7A8B6F;
|
|
80
|
+
--negative-bg: #4E281D;
|
|
81
|
+
--negative-fg: #E86B40;
|
|
82
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
86
|
+
body {
|
|
87
|
+
font-family: var(--font);
|
|
88
|
+
background: var(--surface);
|
|
89
|
+
color: var(--text-primary);
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
min-height: 100vh;
|
|
94
|
+
-webkit-font-smoothing: antialiased;
|
|
95
|
+
}
|
|
96
|
+
.card {
|
|
97
|
+
text-align: center;
|
|
98
|
+
padding: 48px 40px 40px;
|
|
99
|
+
background: var(--surface-card);
|
|
100
|
+
border: 1px solid var(--card-border);
|
|
101
|
+
border-radius: 16px;
|
|
102
|
+
box-shadow: var(--shadow);
|
|
103
|
+
max-width: 380px;
|
|
104
|
+
width: 100%;
|
|
105
|
+
opacity: 0;
|
|
106
|
+
transform: translateY(8px) scale(0.98);
|
|
107
|
+
animation: cardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
|
|
108
|
+
}
|
|
109
|
+
@keyframes cardIn {
|
|
110
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
111
|
+
}
|
|
112
|
+
.icon {
|
|
113
|
+
width: 56px;
|
|
114
|
+
height: 56px;
|
|
115
|
+
margin-bottom: 20px;
|
|
116
|
+
}
|
|
117
|
+
.check {
|
|
118
|
+
stroke-dasharray: 32;
|
|
119
|
+
stroke-dashoffset: 32;
|
|
120
|
+
animation: draw 0.4s ease-out 0.45s forwards;
|
|
121
|
+
}
|
|
122
|
+
.cross {
|
|
123
|
+
stroke-dasharray: 22;
|
|
124
|
+
stroke-dashoffset: 22;
|
|
125
|
+
}
|
|
126
|
+
.cross-1 { animation: draw 0.3s ease-out 0.45s forwards; }
|
|
127
|
+
.cross-2 { animation: draw 0.3s ease-out 0.55s forwards; }
|
|
128
|
+
@keyframes draw {
|
|
129
|
+
to { stroke-dashoffset: 0; }
|
|
130
|
+
}
|
|
131
|
+
h1 {
|
|
132
|
+
font-size: 18px;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
letter-spacing: -0.2px;
|
|
135
|
+
color: var(--text-primary);
|
|
136
|
+
margin-bottom: 6px;
|
|
137
|
+
}
|
|
138
|
+
p {
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
line-height: 1.5;
|
|
141
|
+
color: var(--text-secondary);
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
144
|
+
</head>
|
|
145
|
+
<body>
|
|
146
|
+
<div class="card">
|
|
147
|
+
${success ? checkmarkSvg : errorSvg}
|
|
148
|
+
<h1>${escapeHtml(title)}</h1>
|
|
149
|
+
<p>${subtitle}</p>
|
|
150
|
+
</div>
|
|
151
|
+
</body>
|
|
152
|
+
</html>`;
|
|
153
|
+
}
|
package/src/security/oauth2.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
20
20
|
import { createServer, type Server } from "node:http";
|
|
21
21
|
|
|
22
22
|
import { getLogger } from "../util/logger.js";
|
|
23
|
+
import { renderOAuthCompletionPage as renderLoopbackPage } from "./oauth-completion-page.js";
|
|
23
24
|
|
|
24
25
|
const log = getLogger("oauth2");
|
|
25
26
|
|
|
@@ -359,7 +360,7 @@ function startLoopbackServerAndWaitForCode(
|
|
|
359
360
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
360
361
|
res.end(
|
|
361
362
|
renderLoopbackPage(
|
|
362
|
-
"
|
|
363
|
+
"You can close this tab and return to your assistant.",
|
|
363
364
|
true,
|
|
364
365
|
),
|
|
365
366
|
);
|
|
@@ -433,21 +434,6 @@ function startLoopbackServerAndWaitForCode(
|
|
|
433
434
|
});
|
|
434
435
|
}
|
|
435
436
|
|
|
436
|
-
function escapeHtml(s: string): string {
|
|
437
|
-
return s
|
|
438
|
-
.replace(/&/g, "&")
|
|
439
|
-
.replace(/</g, "<")
|
|
440
|
-
.replace(/>/g, ">")
|
|
441
|
-
.replace(/"/g, """)
|
|
442
|
-
.replace(/'/g, "'");
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function renderLoopbackPage(message: string, success: boolean): string {
|
|
446
|
-
const title = success ? "Authorization Successful" : "Authorization Failed";
|
|
447
|
-
const color = success ? "#4CAF50" : "#f44336";
|
|
448
|
-
return `<!DOCTYPE html><html><head><title>${escapeHtml(title)}</title><style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f5f5f5}div{text-align:center;padding:2rem;background:white;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}h1{color:${color}}</style></head><body><div><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></div></body></html>`;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
437
|
// ---------------------------------------------------------------------------
|
|
452
438
|
// Public API
|
|
453
439
|
// ---------------------------------------------------------------------------
|
|
@@ -640,7 +626,7 @@ function startLoopbackServerForPreparedFlow(
|
|
|
640
626
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
641
627
|
res.end(
|
|
642
628
|
renderLoopbackPage(
|
|
643
|
-
"
|
|
629
|
+
"You can close this tab and return to your assistant.",
|
|
644
630
|
true,
|
|
645
631
|
),
|
|
646
632
|
);
|