@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
@@ -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
- "Authorization successful! You can close this tab.",
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, "&lt;")
440
- .replace(/>/g, "&gt;")
441
- .replace(/"/g, "&quot;")
442
- .replace(/'/g, "&#39;");
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
- "Authorization successful! You can close this tab.",
629
+ "You can close this tab and return to your assistant.",
644
630
  true,
645
631
  ),
646
632
  );
@@ -5,13 +5,16 @@
5
5
  * Backend selection (`resolveBackendAsync`) is the single async decision point:
6
6
  * 1. CES RPC (primary) — injected via `setCesClient()`: delegates credential
7
7
  * operations to the CES process over stdio RPC. This is the default path
8
- * for all local modes (desktop app, dev, CLI).
8
+ * for local modes and the managed bootstrap handshake path.
9
9
  * 2. CES HTTP — containerized mode (IS_CONTAINERIZED + CES_CREDENTIAL_URL):
10
- * delegates to the CES sidecar over HTTP. Used in Docker/managed mode.
10
+ * delegates to the CES sidecar over HTTP. Used in Docker/managed mode,
11
+ * including failover when the bootstrap RPC transport dies later.
11
12
  * 3. Encrypted file store (fallback) — used when CES is unavailable.
12
13
  *
13
14
  * All operations (reads, writes, lists, deletes) go to exactly one backend.
14
- * There are no cross-backend fallbacks or merges.
15
+ * There are no cross-store fallbacks or merges. The only transport failover is
16
+ * CES RPC → CES HTTP in managed mode; both backends target the same CES
17
+ * sidecar and credential data.
15
18
  */
16
19
 
17
20
  import type {
@@ -56,12 +59,59 @@ let _encryptedStore: CredentialBackend | undefined;
56
59
  let _resolvedBackend: CredentialBackend | undefined;
57
60
  let _resolvePromise: Promise<CredentialBackend> | undefined;
58
61
 
62
+ /**
63
+ * Optional callback that attempts to re-establish a CES connection when
64
+ * the current transport dies. Set by lifecycle.ts after initial CES startup.
65
+ * Returns a new CesClient on success, or undefined if reconnection failed.
66
+ */
67
+ let _cesReconnect: (() => Promise<CesClient | undefined>) | undefined;
68
+
69
+ /** Optional listener invoked whenever setCesClient() updates the client. */
70
+ let _cesClientListener: ((client: CesClient | undefined) => void) | undefined;
71
+
72
+ /** Epoch ms of the last reconnection attempt. Used for cooldown. */
73
+ let _lastReconnectAttempt = 0;
74
+
75
+ /** In-flight reconnection promise — concurrent callers share the same attempt. */
76
+ let _reconnectInFlight: Promise<boolean> | undefined;
77
+
78
+ /**
79
+ * Set to true when a ces-http operation returns an unreachable result.
80
+ * Triggers CES RPC reconnection on the next resolveBackendAsync() call so
81
+ * we don't keep routing to a dead HTTP endpoint. Cleared on reconnection or
82
+ * backend reset.
83
+ */
84
+ let _cesHttpUnreachable = false;
85
+
86
+ /** Minimum interval between CES reconnection attempts. */
87
+ const RECONNECT_COOLDOWN_MS = 3_000;
88
+
59
89
  /** Inject a CES RPC client for credential routing. Resets the resolved backend. */
60
90
  export function setCesClient(client: CesClient | undefined): void {
61
91
  _cesClient = client;
62
92
  // Reset resolved backend so next call picks up CES
63
93
  _resolvedBackend = undefined;
64
94
  _resolvePromise = undefined;
95
+ _cesHttpUnreachable = false;
96
+ _cesClientListener?.(client);
97
+ }
98
+
99
+ /**
100
+ * Register a listener that is called whenever setCesClient() updates the
101
+ * CES client reference. Used by lifecycle.ts to keep DaemonServer in sync
102
+ * after a successful reconnection.
103
+ */
104
+ export function onCesClientChanged(
105
+ fn: ((client: CesClient | undefined) => void) | undefined,
106
+ ): void {
107
+ _cesClientListener = fn;
108
+ }
109
+
110
+ /** Register a callback for reconnecting to CES when the transport dies. */
111
+ export function setCesReconnect(
112
+ fn: (() => Promise<CesClient | undefined>) | undefined,
113
+ ): void {
114
+ _cesReconnect = fn;
65
115
  }
66
116
 
67
117
  function getEncryptedStoreBackend(): CredentialBackend {
@@ -77,17 +127,141 @@ function getEncryptedStoreBackend(): CredentialBackend {
77
127
  * 2. Containerized + CES_CREDENTIAL_URL → CES HTTP client (Docker/managed).
78
128
  * 3. Encrypted file store → fallback when CES is unavailable.
79
129
  *
80
- * Once resolved, the backend does not change during the process lifetime.
130
+ * Once resolved, the backend is cached. If it becomes unavailable (e.g. the
131
+ * CES transport dies), we attempt to reconnect via `_cesReconnect` rather
132
+ * than falling back to a different backend. In managed cloud mode CES is the
133
+ * primary credential source — falling back to the encrypted file store would
134
+ * silently serve stale or empty data.
135
+ *
136
+ * In managed mode, if the CES bootstrap RPC transport dies, we first fail
137
+ * over from CES RPC to the CES HTTP credential API exposed by the same
138
+ * sidecar. This avoids pinning credential reads to a dead bootstrap socket
139
+ * when the in-pod HTTP interface is still healthy.
140
+ *
141
+ * If HTTP failover is unavailable, we attempt CES RPC reconnection. When
142
+ * reconnection succeeds the cache is refreshed with the new client.
143
+ *
144
+ * If neither recovery path succeeds, the existing unavailable backend is
145
+ * returned — its methods short-circuit via `isAvailable()` guards and return
146
+ * `unreachable` results so callers can degrade gracefully.
147
+ *
148
+ * Additionally, if CES failed on initial startup (so the encrypted file
149
+ * store became the resolved backend) but the reconnection callback is
150
+ * registered, we periodically attempt to upgrade to CES — ensuring managed
151
+ * cloud deployments don't stay on the (potentially stale) file store.
152
+ *
81
153
  * Call `_resetBackend()` in tests to clear the cached resolution.
82
154
  */
83
155
  async function resolveBackendAsync(): Promise<CredentialBackend> {
84
- if (_resolvedBackend) return _resolvedBackend;
156
+ if (_resolvedBackend) {
157
+ if (!_resolvedBackend.isAvailable()) {
158
+ const cesHttpFallback = tryFailoverToCesHttpBackend(_resolvedBackend);
159
+ if (cesHttpFallback) {
160
+ return cesHttpFallback;
161
+ }
162
+
163
+ // Backend is no longer reachable — attempt CES reconnection.
164
+ const reconnected = await attemptCesReconnection();
165
+ if (reconnected) {
166
+ // setCesClient() cleared the cache — fall through to re-resolve
167
+ // with the fresh client.
168
+ } else {
169
+ // Reconnection failed or on cooldown — return the existing (dead)
170
+ // backend. Its methods short-circuit via isAvailable() guards and
171
+ // return unreachable results. Callers like getProviderKeyAsync fall
172
+ // back to env vars, and embedding backend selection uses cached
173
+ // backends.
174
+ return _resolvedBackend;
175
+ }
176
+ } else if (
177
+ _cesReconnect &&
178
+ (_resolvedBackend.name === "encrypted-store" ||
179
+ (_resolvedBackend.name === "ces-http" && _cesHttpUnreachable))
180
+ ) {
181
+ // CES RPC is the preferred backend. Attempt to reconnect when:
182
+ // - We fell back to the encrypted store (CES startup failed), or
183
+ // - We're on ces-http but an operation returned unreachable (HTTP
184
+ // endpoint is actually down even though isAvailable() returned true,
185
+ // since it only checks env vars, not actual connectivity).
186
+ const reconnected = await attemptCesReconnection();
187
+ if (reconnected) {
188
+ // setCesClient() cleared the cache — fall through to re-resolve.
189
+ } else {
190
+ // Reconnection failed or on cooldown — continue with current backend.
191
+ return _resolvedBackend;
192
+ }
193
+ } else {
194
+ return _resolvedBackend;
195
+ }
196
+ }
85
197
  if (!_resolvePromise) {
86
198
  _resolvePromise = doResolveBackend();
87
199
  }
88
200
  return _resolvePromise;
89
201
  }
90
202
 
203
+ function tryFailoverToCesHttpBackend(
204
+ backend: CredentialBackend,
205
+ ): CredentialBackend | undefined {
206
+ if (backend.name !== "ces-rpc") return undefined;
207
+ if (!getIsContainerized() || !process.env.CES_CREDENTIAL_URL) {
208
+ return undefined;
209
+ }
210
+
211
+ const cesHttp = createCesCredentialBackend();
212
+ if (!cesHttp.isAvailable()) return undefined;
213
+
214
+ _resolvedBackend = cesHttp;
215
+ _resolvePromise = undefined;
216
+ log.warn(
217
+ "CES RPC credential backend became unavailable — failing over to CES HTTP backend",
218
+ );
219
+ return cesHttp;
220
+ }
221
+
222
+ /**
223
+ * Try to re-establish a CES connection when the current transport has died.
224
+ * Returns true if reconnection succeeded (setCesClient was called with a
225
+ * new client), false otherwise.
226
+ *
227
+ * Concurrent callers share the same in-flight reconnection attempt to avoid
228
+ * racing on the same process manager. A timestamp cooldown prevents rapid
229
+ * back-to-back attempts after completion.
230
+ */
231
+ async function attemptCesReconnection(): Promise<boolean> {
232
+ if (!_cesReconnect) return false;
233
+
234
+ // If a reconnection is already in flight, share it.
235
+ if (_reconnectInFlight) return _reconnectInFlight;
236
+
237
+ // Cooldown — don't retry immediately after a completed attempt.
238
+ if (Date.now() - _lastReconnectAttempt < RECONNECT_COOLDOWN_MS) return false;
239
+
240
+ _lastReconnectAttempt = Date.now();
241
+ log.warn("Credential backend unavailable — attempting CES reconnection");
242
+
243
+ _reconnectInFlight = (async () => {
244
+ try {
245
+ const newClient = await _cesReconnect!();
246
+ if (newClient) {
247
+ setCesClient(newClient);
248
+ log.info("CES reconnection successful — credential backend restored");
249
+ return true;
250
+ }
251
+ log.warn("CES reconnection returned no client");
252
+ } catch (err) {
253
+ log.warn({ err }, "CES reconnection failed");
254
+ }
255
+ return false;
256
+ })();
257
+
258
+ try {
259
+ return await _reconnectInFlight;
260
+ } finally {
261
+ _reconnectInFlight = undefined;
262
+ }
263
+ }
264
+
91
265
  async function doResolveBackend(): Promise<CredentialBackend> {
92
266
  // 1. CES RPC — primary credential backend for all local modes
93
267
  if (_cesClient) {
@@ -119,6 +293,21 @@ async function doResolveBackend(): Promise<CredentialBackend> {
119
293
  return _resolvedBackend;
120
294
  }
121
295
 
296
+ /**
297
+ * Update the ces-http reachability latch after any get/list operation.
298
+ * Sets `_cesHttpUnreachable = true` on failure so the next
299
+ * resolveBackendAsync() call triggers a CES RPC reconnection attempt.
300
+ * Clears it on success so a transient blip doesn't cause endless churn.
301
+ */
302
+ function updateCesHttpReachability(
303
+ backend: CredentialBackend,
304
+ unreachable: boolean,
305
+ ): void {
306
+ if (backend.name === "ces-http") {
307
+ _cesHttpUnreachable = unreachable;
308
+ }
309
+ }
310
+
122
311
  /**
123
312
  * List all account names from the resolved backend (async).
124
313
  *
@@ -126,7 +315,9 @@ async function doResolveBackend(): Promise<CredentialBackend> {
126
315
  */
127
316
  export async function listSecureKeysAsync(): Promise<CredentialListResult> {
128
317
  const backend = await resolveBackendAsync();
129
- return backend.list();
318
+ const result = await backend.list();
319
+ updateCesHttpReachability(backend, result.unreachable);
320
+ return result;
130
321
  }
131
322
 
132
323
  // ---------------------------------------------------------------------------
@@ -147,6 +338,7 @@ export async function getSecureKeyResultAsync(
147
338
  ): Promise<SecureKeyResult> {
148
339
  const backend = await resolveBackendAsync();
149
340
  const result = await backend.get(account);
341
+ updateCesHttpReachability(backend, result.unreachable);
150
342
  if (result.value != null) {
151
343
  return { value: result.value, unreachable: false };
152
344
  }
@@ -180,6 +372,7 @@ export async function setSecureKeyAsync(
180
372
  "Credential backend set failed",
181
373
  );
182
374
  }
375
+ updateCesHttpReachability(backend, !ok);
183
376
  return ok;
184
377
  }
185
378
 
@@ -192,7 +385,9 @@ export async function deleteSecureKeyAsync(
192
385
  account: string,
193
386
  ): Promise<DeleteResult> {
194
387
  const backend = await resolveBackendAsync();
195
- return backend.delete(account);
388
+ const result = await backend.delete(account);
389
+ updateCesHttpReachability(backend, result === "error");
390
+ return result;
196
391
  }
197
392
 
198
393
  // ---------------------------------------------------------------------------
@@ -264,4 +459,9 @@ export function _resetBackend(): void {
264
459
  _encryptedStore = undefined;
265
460
  _resolvedBackend = undefined;
266
461
  _resolvePromise = undefined;
462
+ _cesReconnect = undefined;
463
+ _cesClientListener = undefined;
464
+ _lastReconnectAttempt = 0;
465
+ _reconnectInFlight = undefined;
466
+ _cesHttpUnreachable = false;
267
467
  }
@@ -35,16 +35,13 @@ import { getSecureKeyAsync, setSecureKeyAsync } from "./secure-keys.js";
35
35
 
36
36
  const log = getLogger("token-manager");
37
37
 
38
- const MESSAGING_SERVICES = new Set(["integration:google", "integration:slack"]);
38
+ const MESSAGING_SERVICES = new Set(["google", "slack"]);
39
39
 
40
40
  function recoveryHint(service: string): string {
41
- const shortName = service.startsWith("integration:")
42
- ? service.slice("integration:".length)
43
- : service;
44
41
  if (MESSAGING_SERVICES.has(service)) {
45
- return ` Reconnect ${shortName} — follow the Error Recovery steps in the messaging skill. Do not present options or explain the error to the user.`;
42
+ return ` Reconnect ${service} — follow the Error Recovery steps in the messaging skill. Do not present options or explain the error to the user.`;
46
43
  }
47
- return ` Re-authorization required for ${shortName}. Do not present options or explain the error to the user.`;
44
+ return ` Re-authorization required for ${service}. Do not present options or explain the error to the user.`;
48
45
  }
49
46
 
50
47
  // ── Shared circuit breaker & deduplication instances ──────────────────
@@ -138,11 +138,16 @@ export function handleBashSignal(filename: string): void {
138
138
  const child = spawn("bash", ["-c", command], {
139
139
  cwd: getWorkspaceDir(),
140
140
  stdio: ["ignore", "pipe", "pipe"],
141
+ detached: true,
141
142
  });
142
143
 
143
144
  const timer = setTimeout(() => {
144
145
  timedOut = true;
145
- child.kill("SIGKILL");
146
+ try {
147
+ process.kill(-child.pid!, "SIGKILL");
148
+ } catch {
149
+ // Process group may have already exited.
150
+ }
146
151
  }, effectiveTimeout);
147
152
 
148
153
  child.stdout.on("data", (data: Buffer) => {
@@ -0,0 +1,44 @@
1
+ import { getLogger } from "../util/logger.js";
2
+ import type { CatalogSkill } from "./catalog-install.js";
3
+ import {
4
+ fetchCatalog,
5
+ getRepoSkillsDir,
6
+ readLocalCatalog,
7
+ } from "./catalog-install.js";
8
+
9
+ const log = getLogger("catalog-cache");
10
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
11
+
12
+ let cachedCatalog: CatalogSkill[] | null = null;
13
+ let cacheTimestamp = 0;
14
+
15
+ /** Resolve the Vellum catalog with in-memory caching. */
16
+ export async function getCatalog(): Promise<CatalogSkill[]> {
17
+ if (cachedCatalog && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
18
+ return cachedCatalog;
19
+ }
20
+ const repoSkillsDir = getRepoSkillsDir();
21
+ let catalog: CatalogSkill[];
22
+ if (repoSkillsDir) {
23
+ catalog = readLocalCatalog(repoSkillsDir);
24
+ } else {
25
+ try {
26
+ catalog = await fetchCatalog();
27
+ } catch (err) {
28
+ log.warn(
29
+ { err },
30
+ "Failed to fetch Vellum catalog, using stale cache or empty",
31
+ );
32
+ return cachedCatalog ?? [];
33
+ }
34
+ }
35
+ cachedCatalog = catalog;
36
+ cacheTimestamp = Date.now();
37
+ return catalog;
38
+ }
39
+
40
+ /** Invalidate the cache (for testing or forced refresh). */
41
+ export function invalidateCatalogCache(): void {
42
+ cachedCatalog = null;
43
+ cacheTimestamp = 0;
44
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared catalog text-search helper.
3
+ *
4
+ * Both the CLI `skills search` command and the daemon `searchSkills` handler
5
+ * need case-insensitive substring matching across multiple fields. This module
6
+ * provides a single generic implementation to prevent the two from drifting.
7
+ */
8
+
9
+ export function filterByQuery<T>(
10
+ items: T[],
11
+ query: string,
12
+ fields: ((item: T) => string)[],
13
+ ): T[] {
14
+ const lower = query.toLowerCase();
15
+ return items.filter((item) =>
16
+ fields.some((fn) => fn(item).toLowerCase().includes(lower)),
17
+ );
18
+ }
@@ -242,13 +242,13 @@ class BrowserManager {
242
242
  if (!chromiumInstalled) {
243
243
  log.info("Chromium not installed, installing via playwright...");
244
244
  const proc = Bun.spawn(
245
- ["bunx", "playwright", "install", "chromium"],
245
+ ["bunx", "playwright", "install", "--with-deps", "chromium"],
246
246
  {
247
247
  stdout: "pipe",
248
248
  stderr: "pipe",
249
249
  },
250
250
  );
251
- const timeoutMs = 120_000;
251
+ const timeoutMs = 300_000;
252
252
  let timer: ReturnType<typeof setTimeout>;
253
253
  const exitCode = await Promise.race([
254
254
  proc.exited.finally(() => clearTimeout(timer)),
@@ -48,7 +48,7 @@ async function slackWelcomeDM(ctx: PostConnectHookContext): Promise<void> {
48
48
  // ---------------------------------------------------------------------------
49
49
 
50
50
  const POST_CONNECT_HOOKS: Record<string, PostConnectHook> = {
51
- "integration:slack": slackWelcomeDM,
51
+ slack: slackWelcomeDM,
52
52
  };
53
53
 
54
54
  /**
@@ -11,12 +11,8 @@ import {
11
11
  getAppByProviderAndClientId,
12
12
  getMostRecentAppByProvider,
13
13
  getProvider,
14
+ listProviders,
14
15
  } from "../../oauth/oauth-store.js";
15
- import {
16
- getProviderBehavior,
17
- resolveService,
18
- SERVICE_ALIASES,
19
- } from "../../oauth/provider-behaviors.js";
20
16
  import { RiskLevel } from "../../permissions/types.js";
21
17
  import type { ToolDefinition } from "../../providers/types.js";
22
18
  import { buildAssistantEvent } from "../../runtime/assistant-event.js";
@@ -103,11 +99,11 @@ class CredentialStoreTool implements Tool {
103
99
  "describe",
104
100
  ],
105
101
  description:
106
- 'The operation to perform. Use "prompt" to ask the user for a secret via secure UI - the value never enters the conversation. Use "oauth2_connect" to connect an OAuth2 service via browser authorization. Use "describe" to get setup metadata for a well-known OAuth service (dashboard URL, scopes, redirect URI, etc.). For well-known services (gmail, slack), only the service name is required - endpoints, scopes, and stored client credentials are resolved automatically.',
102
+ 'The operation to perform. Use "prompt" to ask the user for a secret via secure UI - the value never enters the conversation. Use "oauth2_connect" to connect an OAuth2 service via browser authorization. Use "describe" to get setup metadata for a well-known OAuth service (dashboard URL, scopes, redirect URI, etc.). For well-known services (google, slack), only the service name is required - endpoints, scopes, and stored client credentials are resolved automatically.',
107
103
  },
108
104
  service: {
109
105
  type: "string",
110
- description: "Service name, e.g. gmail, github",
106
+ description: "Service name, e.g. google, github",
111
107
  },
112
108
  account: {
113
109
  type: "string",
@@ -158,7 +154,7 @@ class CredentialStoreTool implements Tool {
158
154
  type: "array",
159
155
  items: { type: "string" },
160
156
  description:
161
- "OAuth2 scopes to request (only for oauth2_connect action). Auto-filled for well-known services (gmail, slack).",
157
+ "OAuth2 scopes to request (only for oauth2_connect action). Auto-filled for well-known services (google, slack).",
162
158
  },
163
159
  client_id: {
164
160
  type: "string",
@@ -822,18 +818,13 @@ class CredentialStoreTool implements Tool {
822
818
  }
823
819
 
824
820
  case "oauth2_connect": {
825
- const rawService = input.service as string | undefined;
826
- if (!rawService)
821
+ const service = input.service as string | undefined;
822
+ if (!service)
827
823
  return {
828
824
  content: "Error: service is required for oauth2_connect action",
829
825
  isError: true,
830
826
  };
831
827
 
832
- // Resolve aliases (e.g. "gmail" → "integration:google")
833
- const service = resolveService(rawService);
834
-
835
- // Code-side behavioral fields (identityVerifier, setup, etc.)
836
- const behavior = getProviderBehavior(service);
837
828
  // Protocol-level config from the DB (authUrl, tokenUrl, scopes, etc.)
838
829
  const providerRow = getProvider(service);
839
830
 
@@ -880,16 +871,10 @@ class CredentialStoreTool implements Tool {
880
871
  // Fail early when client_secret is required but missing - guide the
881
872
  // agent to collect it from the user rather than letting it improvise
882
873
  // browser-automation workarounds that inevitably fail.
883
- const requiresSecret =
884
- behavior?.setup?.requiresClientSecret ??
885
- !!(providerRow.tokenEndpointAuthMethod || providerRow.extraParams);
874
+ const requiresSecret = !!providerRow.requiresClientSecret;
886
875
  if (requiresSecret && !clientSecret) {
887
- const skillId = behavior?.setupSkillId;
888
- const skillHint = skillId
889
- ? `\n\nLoad the "${skillId}" skill for provider-specific instructions on obtaining the client secret.`
890
- : '\n\nUse credential_store with action "prompt" to securely collect the client_secret from the user before calling oauth2_connect again.';
891
876
  return {
892
- content: `Error: client_secret is required for ${rawService} but not found in the vault.${skillHint}`,
877
+ content: `Error: client_secret is required for ${service} but not found in the vault.\n\nUse credential_store with action "prompt" to securely collect the client_secret from the user before calling oauth2_connect again.`,
893
878
  isError: true,
894
879
  };
895
880
  }
@@ -897,7 +882,7 @@ class CredentialStoreTool implements Tool {
897
882
  // Delegate to the shared orchestrator - it resolves authUrl, tokenUrl,
898
883
  // extraParams, userinfoUrl, and tokenEndpointAuthMethod from the DB.
899
884
  const result = await orchestrateOAuthConnect({
900
- service: rawService,
885
+ service,
901
886
  clientId,
902
887
  clientSecret,
903
888
  isInteractive: !!context.isInteractive,
@@ -949,7 +934,7 @@ class CredentialStoreTool implements Tool {
949
934
 
950
935
  if (result.deferred) {
951
936
  return {
952
- content: `To connect ${rawService}, open this link and authorize access:\n\n${result.authUrl}\n\nOnce you authorize, the connection will be set up automatically. You can verify by asking me to check your inbox.`,
937
+ content: `To connect ${service}, open this link and authorize access:\n\n${result.authUrl}\n\nOnce you authorize, the connection will be set up automatically. You can verify by asking me to check your inbox.`,
953
938
  isError: false,
954
939
  };
955
940
  }
@@ -963,34 +948,30 @@ class CredentialStoreTool implements Tool {
963
948
  }
964
949
 
965
950
  case "describe": {
966
- const rawService = (input.service as string | undefined) ?? "";
967
- if (!rawService) {
951
+ const descService = (input.service as string | undefined) ?? "";
952
+ if (!descService) {
968
953
  return {
969
954
  content: "Error: service is required for describe action",
970
955
  isError: true,
971
956
  };
972
957
  }
973
- const resolvedService = resolveService(rawService);
974
- const descProviderRow = getProvider(resolvedService);
958
+ const descProviderRow = getProvider(descService);
975
959
  if (!descProviderRow) {
960
+ const availableServices = listProviders().map((p) => p.providerKey);
976
961
  return {
977
- content: `No well-known OAuth config found for "${rawService}". Available services: ${Object.keys(
978
- SERVICE_ALIASES,
979
- ).join(", ")}`,
962
+ content: `No well-known OAuth config found for "${descService}". Available services: ${availableServices.join(", ")}`,
980
963
  isError: false,
981
964
  };
982
965
  }
983
966
 
984
- const descBehavior = getProviderBehavior(resolvedService);
985
-
986
967
  // Compute the redirect URI based on callback transport
987
968
  let redirectUri: string;
988
969
  const transport =
989
970
  (descProviderRow.callbackTransport as
990
971
  | "loopback"
991
972
  | "gateway"
992
- | null) ?? "gateway";
993
- const loopbackPort = descBehavior?.loopbackPort;
973
+ | null) ?? "loopback";
974
+ const loopbackPort = descProviderRow.loopbackPort;
994
975
  if (transport === "loopback" && loopbackPort) {
995
976
  redirectUri = `http://localhost:${loopbackPort}/oauth/callback`;
996
977
  } else if (transport === "loopback") {
@@ -1010,20 +991,14 @@ class CredentialStoreTool implements Tool {
1010
991
  }
1011
992
  }
1012
993
 
1013
- // Prefer explicit setup metadata, fall back to heuristic
1014
- const requiresClientSecret =
1015
- descBehavior?.setup?.requiresClientSecret ??
1016
- !!(
1017
- descProviderRow.tokenEndpointAuthMethod ||
1018
- descProviderRow.extraParams
1019
- );
994
+ const requiresClientSecret = !!descProviderRow.requiresClientSecret;
1020
995
 
1021
996
  const descDefaultScopes: string[] = descProviderRow.defaultScopes
1022
997
  ? JSON.parse(descProviderRow.defaultScopes)
1023
998
  : [];
1024
999
 
1025
1000
  const info: Record<string, unknown> = {
1026
- service: resolvedService,
1001
+ service: descService,
1027
1002
  authUrl: descProviderRow.authUrl,
1028
1003
  tokenUrl: descProviderRow.tokenUrl,
1029
1004
  scopes: descDefaultScopes,
@@ -1031,7 +1006,21 @@ class CredentialStoreTool implements Tool {
1031
1006
  redirectUri,
1032
1007
  requiresClientSecret,
1033
1008
  };
1034
- if (descBehavior?.setup) info.setup = descBehavior.setup;
1009
+ if (
1010
+ descProviderRow.displayName &&
1011
+ descProviderRow.dashboardUrl &&
1012
+ descProviderRow.appType
1013
+ ) {
1014
+ info.setup = {
1015
+ displayName: descProviderRow.displayName,
1016
+ dashboardUrl: descProviderRow.dashboardUrl,
1017
+ appType: descProviderRow.appType,
1018
+ requiresClientSecret: !!descProviderRow.requiresClientSecret,
1019
+ ...(descProviderRow.setupNotes
1020
+ ? { notes: JSON.parse(descProviderRow.setupNotes) }
1021
+ : {}),
1022
+ };
1023
+ }
1035
1024
  if (descProviderRow.extraParams) {
1036
1025
  try {
1037
1026
  info.extraParams = JSON.parse(descProviderRow.extraParams);