@vellumai/assistant 0.5.11 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -447,10 +447,11 @@ export async function enforceIngressAcl(
447
447
  );
448
448
  }
449
449
 
450
+ const replyText = guardianNotified
451
+ ? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
452
+ : "Sorry, you haven't been approved to message this assistant.";
453
+ let replyDelivered = false;
450
454
  if (replyCallbackUrl) {
451
- const replyText = guardianNotified
452
- ? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
453
- : "Sorry, you haven't been approved to message this assistant.";
454
455
  const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
455
456
  chatId: conversationExternalId,
456
457
  text: replyText,
@@ -467,6 +468,7 @@ export async function enforceIngressAcl(
467
468
  replyPayload,
468
469
  mintBearerToken(),
469
470
  );
471
+ replyDelivered = true;
470
472
  } catch (err) {
471
473
  log.error(
472
474
  { err, conversationExternalId },
@@ -481,6 +483,9 @@ export async function enforceIngressAcl(
481
483
  accepted: true,
482
484
  denied: true,
483
485
  reason: "not_a_member",
486
+ // Include reply text so the gateway can deliver directly when
487
+ // callback delivery failed (e.g. signing-key mismatch → 401).
488
+ ...(!replyDelivered && { replyText }),
484
489
  }),
485
490
  guardianVerifyCode,
486
491
  };
@@ -714,15 +719,16 @@ export async function enforceIngressAcl(
714
719
  }
715
720
  }
716
721
 
722
+ const inactiveReplyText = guardianNotified
723
+ ? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
724
+ : "Sorry, you haven't been approved to message this assistant.";
725
+ let inactiveReplyDelivered = false;
717
726
  if (replyCallbackUrl) {
718
- const replyText = guardianNotified
719
- ? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
720
- : "Sorry, you haven't been approved to message this assistant.";
721
727
  const inactiveReplyPayload: Parameters<
722
728
  typeof deliverChannelReply
723
729
  >[1] = {
724
730
  chatId: conversationExternalId,
725
- text: replyText,
731
+ text: inactiveReplyText,
726
732
  assistantId,
727
733
  };
728
734
  // On Slack, send as ephemeral so only the requester sees the rejection
@@ -739,6 +745,7 @@ export async function enforceIngressAcl(
739
745
  inactiveReplyPayload,
740
746
  mintBearerToken(),
741
747
  );
748
+ inactiveReplyDelivered = true;
742
749
  } catch (err) {
743
750
  log.error(
744
751
  { err, conversationExternalId },
@@ -752,6 +759,7 @@ export async function enforceIngressAcl(
752
759
  accepted: true,
753
760
  denied: true,
754
761
  reason: `member_${channelStatusToMemberStatus(resolvedMember.channel.status)}`,
762
+ ...(!inactiveReplyDelivered && { replyText: inactiveReplyText }),
755
763
  }),
756
764
  guardianVerifyCode,
757
765
  };
@@ -763,10 +771,13 @@ export async function enforceIngressAcl(
763
771
  { sourceChannel, channelId: resolvedMember.channel.id },
764
772
  "Ingress ACL: member policy deny",
765
773
  );
774
+ const denyReplyText =
775
+ "Sorry, you haven't been approved to message this assistant.";
776
+ let denyReplyDelivered = false;
766
777
  if (replyCallbackUrl) {
767
778
  const denyPayload: Parameters<typeof deliverChannelReply>[1] = {
768
779
  chatId: conversationExternalId,
769
- text: "Sorry, you haven't been approved to message this assistant.",
780
+ text: denyReplyText,
770
781
  assistantId,
771
782
  };
772
783
  if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
@@ -779,6 +790,7 @@ export async function enforceIngressAcl(
779
790
  denyPayload,
780
791
  mintBearerToken(),
781
792
  );
793
+ denyReplyDelivered = true;
782
794
  } catch (err) {
783
795
  log.error(
784
796
  { err, conversationExternalId },
@@ -792,6 +804,7 @@ export async function enforceIngressAcl(
792
804
  accepted: true,
793
805
  denied: true,
794
806
  reason: "policy_deny",
807
+ ...(!denyReplyDelivered && { replyText: denyReplyText }),
795
808
  }),
796
809
  guardianVerifyCode,
797
810
  };
@@ -28,7 +28,7 @@ const log = getLogger("slack-share");
28
28
  * Resolve the Slack bot token from the OAuth connection store.
29
29
  */
30
30
  async function resolveSlackToken(): Promise<string | undefined> {
31
- const conn = getConnectionByProvider("integration:slack");
31
+ const conn = getConnectionByProvider("slack");
32
32
  return conn
33
33
  ? await getSecureKeyAsync(`oauth_connection/${conn.id}/access_token`)
34
34
  : undefined;
@@ -81,7 +81,8 @@ export function oauthAppsRouteDefinitions(): RouteDefinition[] {
81
81
  description: providerRow.description ?? null,
82
82
  dashboard_url: providerRow.dashboardUrl ?? null,
83
83
  client_id_placeholder: providerRow.clientIdPlaceholder ?? null,
84
- requires_client_secret: providerRow.requiresClientSecret ?? 1,
84
+ requires_client_secret: !!(providerRow.requiresClientSecret ?? 1),
85
+ supports_managed_mode: !!providerRow.managedServiceConfigKey,
85
86
  }
86
87
  : null;
87
88
 
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import {
4
+ getPlatformAssistantId,
4
5
  setPlatformAssistantId,
5
6
  setPlatformBaseUrl,
6
7
  setPlatformOrganizationId,
@@ -86,7 +87,10 @@ async function queueApiKeyPropagation(
86
87
  return;
87
88
  }
88
89
  try {
89
- await cesClient.updateAssistantApiKey(apiKey);
90
+ await cesClient.updateAssistantApiKey(
91
+ apiKey,
92
+ getPlatformAssistantId() || undefined,
93
+ );
90
94
  log.info(
91
95
  "Pushed queued assistant API key to CES after handshake completed",
92
96
  );
@@ -106,7 +110,7 @@ async function queueApiKeyPropagation(
106
110
 
107
111
  export async function handleAddSecret(
108
112
  req: Request,
109
- getCesClient?: () => CesClient | undefined,
113
+ deps?: SecretRouteDeps,
110
114
  ): Promise<Response> {
111
115
  let body: { type?: string; name?: string; value?: string };
112
116
  try {
@@ -192,9 +196,7 @@ export async function handleAddSecret(
192
196
  500,
193
197
  );
194
198
  }
195
- clearEmbeddingBackendCache();
196
- invalidateConfigCache();
197
- await initializeProviders(getConfig());
199
+ await refreshProvidersAfterSecretChange(deps);
198
200
  log.info({ provider: name }, "API key updated via HTTP");
199
201
  return Response.json({ success: true, type, name }, { status: 201 });
200
202
  }
@@ -275,16 +277,19 @@ export async function handleAddSecret(
275
277
  }
276
278
  }
277
279
  if (isManagedProxyCredential(service, field)) {
278
- await initializeProviders(getConfig());
280
+ await refreshProvidersAfterSecretChange(deps);
279
281
  if (service === "vellum" && field === "assistant_api_key") {
280
282
  // Push the API key to CES so managed credential materialization
281
283
  // works even though the handshake ran before the key was available.
282
284
  const generation = ++apiKeyGeneration;
283
- const cesClient = getCesClient?.();
285
+ const cesClient = deps?.getCesClient?.();
284
286
  if (cesClient) {
285
287
  if (cesClient.isReady()) {
286
288
  try {
287
- await cesClient.updateAssistantApiKey(value);
289
+ await cesClient.updateAssistantApiKey(
290
+ value,
291
+ getPlatformAssistantId() || undefined,
292
+ );
288
293
  log.info(
289
294
  "Pushed assistant API key to CES after managed proxy credential update",
290
295
  );
@@ -394,7 +399,10 @@ export async function handleReadSecret(req: Request): Promise<Response> {
394
399
  }
395
400
  }
396
401
 
397
- export async function handleDeleteSecret(req: Request): Promise<Response> {
402
+ export async function handleDeleteSecret(
403
+ req: Request,
404
+ deps?: SecretRouteDeps,
405
+ ): Promise<Response> {
398
406
  let body: { type?: string; name?: string };
399
407
  try {
400
408
  body = (await req.json()) as { type?: string; name?: string };
@@ -438,9 +446,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
438
446
  500,
439
447
  );
440
448
  }
441
- clearEmbeddingBackendCache();
442
- invalidateConfigCache();
443
- await initializeProviders(getConfig());
449
+ await refreshProvidersAfterSecretChange(deps);
444
450
  log.info({ provider: name }, "API key deleted via HTTP");
445
451
  return Response.json({ success: true, type, name });
446
452
  }
@@ -488,7 +494,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
488
494
  setSentryUserId(undefined);
489
495
  }
490
496
  if (isManagedProxyCredential(service, field)) {
491
- await initializeProviders(getConfig());
497
+ await refreshProvidersAfterSecretChange(deps);
492
498
  }
493
499
  log.info({ service, field }, "Credential deleted via HTTP");
494
500
  return Response.json({ success: true, type, name });
@@ -548,6 +554,30 @@ export async function handleListSecrets(): Promise<Response> {
548
554
  export interface SecretRouteDeps {
549
555
  /** Accessor for the CES client, used to push API key updates after hatch. */
550
556
  getCesClient?: () => CesClient | undefined;
557
+ /**
558
+ * Called after provider-affecting credentials change so live conversations
559
+ * can be reloaded with fresh provider instances.
560
+ */
561
+ onProviderCredentialsChanged?: () => void | Promise<void>;
562
+ }
563
+
564
+ async function refreshProvidersAfterSecretChange(
565
+ deps?: SecretRouteDeps,
566
+ ): Promise<void> {
567
+ clearEmbeddingBackendCache();
568
+ invalidateConfigCache();
569
+ await initializeProviders(getConfig());
570
+
571
+ if (!deps?.onProviderCredentialsChanged) return;
572
+
573
+ try {
574
+ await deps.onProviderCredentialsChanged();
575
+ } catch (err) {
576
+ log.warn(
577
+ { error: err instanceof Error ? err.message : String(err) },
578
+ "Failed to refresh live conversations after provider credential change",
579
+ );
580
+ }
551
581
  }
552
582
 
553
583
  export function secretRouteDefinitions(
@@ -557,7 +587,7 @@ export function secretRouteDefinitions(
557
587
  {
558
588
  endpoint: "secrets",
559
589
  method: "POST",
560
- handler: async ({ req }) => handleAddSecret(req, deps?.getCesClient),
590
+ handler: async ({ req }) => handleAddSecret(req, deps),
561
591
  summary: "Add a secret",
562
592
  description:
563
593
  "Store a new secret (API key, OAuth token, etc.) in the credential vault.",
@@ -576,7 +606,7 @@ export function secretRouteDefinitions(
576
606
  {
577
607
  endpoint: "secrets",
578
608
  method: "DELETE",
579
- handler: async ({ req }) => handleDeleteSecret(req),
609
+ handler: async ({ req }) => handleDeleteSecret(req, deps),
580
610
  summary: "Delete a secret",
581
611
  description: "Remove a secret from the credential vault by name.",
582
612
  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 resolvedService = resolveService(body.service);
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(resolvedService);
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(resolvedService);
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 "${body.service}". Store it first via the credential vault.`,
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 behavior = getProviderBehavior(resolvedService);
211
- const providerRow = getProvider(resolvedService);
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 "${body.service}" but not found in the credential store. Store it first via the credential vault.`,
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: body.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(resolvedService);
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: body.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(resolvedService);
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: body.service }, "OAuth connect flow failed");
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: "Return all installed skills.",
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.array(z.unknown()).describe("Skill objects"),
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 skills = listSkills(ctx());
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("integration:google"),
18
+ isConnected: () => isProviderConnected("google"),
19
19
  },
20
20
  {
21
21
  name: "Slack",
22
22
  category: "messaging",
23
- isConnected: () => isProviderConnected("integration:slack"),
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
- CesRpcMethod.GetCredential,
36
- { account },
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
- CesRpcMethod.SetCredential,
52
- { account, value },
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
- CesRpcMethod.DeleteCredential,
65
- { account },
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, "&amp;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;")
11
+ .replace(/"/g, "&quot;")
12
+ .replace(/'/g, "&#39;");
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
+ }