@vellumai/assistant 0.4.50 → 0.4.51

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 (153) hide show
  1. package/docs/architecture/integrations.md +2 -2
  2. package/docs/architecture/keychain-broker.md +6 -6
  3. package/knip.json +32 -0
  4. package/package.json +3 -2
  5. package/src/__tests__/btw-routes.test.ts +61 -5
  6. package/src/__tests__/config-watcher.test.ts +8 -0
  7. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  8. package/src/__tests__/credential-vault-unit.test.ts +19 -18
  9. package/src/__tests__/credential-vault.test.ts +17 -17
  10. package/src/__tests__/credentials-cli.test.ts +257 -82
  11. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  12. package/src/__tests__/integration-status.test.ts +31 -30
  13. package/src/__tests__/invite-redemption-service.test.ts +121 -32
  14. package/src/__tests__/invite-routes-http.test.ts +166 -5
  15. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  16. package/src/__tests__/oauth-cli.test.ts +286 -60
  17. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  18. package/src/__tests__/oauth-store.test.ts +243 -11
  19. package/src/__tests__/relay-server.test.ts +9 -0
  20. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  21. package/src/__tests__/secure-keys.test.ts +71 -16
  22. package/src/__tests__/server-history-render.test.ts +2 -2
  23. package/src/__tests__/skills.test.ts +2 -2
  24. package/src/__tests__/slack-channel-config.test.ts +10 -8
  25. package/src/__tests__/twilio-config.test.ts +11 -10
  26. package/src/__tests__/twilio-provider.test.ts +9 -4
  27. package/src/__tests__/voice-invite-redemption.test.ts +58 -9
  28. package/src/calls/call-domain.ts +3 -4
  29. package/src/calls/relay-server.ts +1 -1
  30. package/src/calls/twilio-config.ts +4 -3
  31. package/src/calls/twilio-provider.ts +14 -9
  32. package/src/calls/twilio-rest.ts +10 -7
  33. package/src/cli/commands/config.ts +14 -9
  34. package/src/cli/commands/contacts.ts +3 -0
  35. package/src/cli/commands/credentials.ts +170 -174
  36. package/src/cli/commands/doctor.ts +7 -5
  37. package/src/cli/commands/keys.ts +9 -9
  38. package/src/cli/commands/oauth/apps.ts +40 -11
  39. package/src/cli/commands/oauth/connections.ts +66 -30
  40. package/src/cli/commands/oauth/index.ts +3 -3
  41. package/src/cli/commands/oauth/providers.ts +3 -3
  42. package/src/cli.ts +16 -12
  43. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  44. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  45. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  46. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  47. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  48. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  49. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  50. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  51. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  52. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  53. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  54. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  55. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  56. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  57. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  58. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  59. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  60. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  61. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  62. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  63. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  64. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  65. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  66. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  67. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  68. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  69. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  70. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  71. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  72. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  73. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  74. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  75. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  76. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  77. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  78. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  79. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  80. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  81. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  82. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  83. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  84. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  85. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  86. package/src/config/loader.ts +6 -42
  87. package/src/contacts/contact-store.ts +39 -2
  88. package/src/contacts/contacts-write.ts +9 -0
  89. package/src/daemon/config-watcher.ts +8 -13
  90. package/src/daemon/handlers/config-ingress.ts +2 -2
  91. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  92. package/src/daemon/handlers/config-telegram.ts +23 -14
  93. package/src/daemon/handlers/session-history.ts +1 -358
  94. package/src/daemon/handlers/shared.ts +3 -17
  95. package/src/daemon/lifecycle.ts +8 -1
  96. package/src/daemon/message-types/sessions.ts +0 -42
  97. package/src/daemon/server.ts +0 -6
  98. package/src/daemon/session-slash.ts +3 -5
  99. package/src/email/providers/index.ts +2 -2
  100. package/src/media/avatar-router.ts +1 -1
  101. package/src/memory/conversation-queries.ts +3 -80
  102. package/src/memory/db-init.ts +4 -0
  103. package/src/memory/invite-store.ts +19 -0
  104. package/src/memory/migrations/149-oauth-tables.ts +1 -1
  105. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +1 -1
  106. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  107. package/src/memory/migrations/index.ts +1 -0
  108. package/src/memory/migrations/registry.ts +6 -0
  109. package/src/memory/schema/contacts.ts +1 -0
  110. package/src/messaging/provider.ts +1 -1
  111. package/src/messaging/providers/gmail/adapter.ts +1 -1
  112. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  113. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  114. package/src/messaging/registry.ts +9 -5
  115. package/src/oauth/byo-connection.test.ts +32 -24
  116. package/src/oauth/connect-orchestrator.ts +4 -10
  117. package/src/oauth/connection-resolver.ts +20 -6
  118. package/src/oauth/manual-token-connection.ts +5 -5
  119. package/src/oauth/oauth-store.ts +83 -17
  120. package/src/oauth/platform-connection.test.ts +1 -1
  121. package/src/oauth/provider-behaviors.ts +503 -4
  122. package/src/oauth/seed-providers.ts +208 -8
  123. package/src/oauth/token-persistence.ts +20 -13
  124. package/src/runtime/channel-readiness-service.ts +48 -40
  125. package/src/runtime/http-types.ts +2 -0
  126. package/src/runtime/invite-redemption-service.ts +71 -29
  127. package/src/runtime/invite-service.ts +40 -22
  128. package/src/runtime/middleware/twilio-validation.ts +1 -1
  129. package/src/runtime/routes/btw-routes.ts +10 -5
  130. package/src/runtime/routes/conversation-routes.ts +47 -10
  131. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  132. package/src/runtime/routes/integrations/telegram.ts +2 -2
  133. package/src/runtime/routes/integrations/twilio.ts +17 -17
  134. package/src/runtime/routes/invite-routes.ts +29 -4
  135. package/src/runtime/routes/secret-routes.ts +17 -0
  136. package/src/runtime/routes/settings-routes.ts +3 -3
  137. package/src/runtime/routes/workspace-routes.ts +7 -3
  138. package/src/runtime/routes/workspace-utils.ts +8 -2
  139. package/src/schedule/integration-status.ts +26 -19
  140. package/src/security/oauth2.ts +6 -7
  141. package/src/security/secure-keys.ts +19 -16
  142. package/src/security/token-manager.ts +13 -6
  143. package/src/services/vercel-deploy.ts +0 -24
  144. package/src/signals/confirm.ts +78 -0
  145. package/src/signals/mcp-reload.ts +18 -0
  146. package/src/tools/credentials/vault.ts +22 -5
  147. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  148. package/src/tools/schedule/create.ts +2 -2
  149. package/src/watcher/provider-types.ts +1 -1
  150. package/src/watcher/providers/github.ts +1 -1
  151. package/src/watcher/providers/gmail.ts +3 -3
  152. package/src/watcher/providers/google-calendar.ts +3 -3
  153. package/src/watcher/providers/linear.ts +1 -1
@@ -20,6 +20,17 @@ import { httpError } from "../http-errors.js";
20
20
  import type { RouteDefinition } from "../http-router.js";
21
21
 
22
22
  const log = getLogger("runtime-http");
23
+ const MANAGED_PROXY_CREDENTIAL = {
24
+ service: "vellum",
25
+ field: "assistant_api_key",
26
+ };
27
+
28
+ function isManagedProxyCredential(service: string, field: string): boolean {
29
+ return (
30
+ service === MANAGED_PROXY_CREDENTIAL.service &&
31
+ field === MANAGED_PROXY_CREDENTIAL.field
32
+ );
33
+ }
23
34
 
24
35
  export async function handleAddSecret(req: Request): Promise<Response> {
25
36
  const body = (await req.json()) as {
@@ -89,6 +100,9 @@ export async function handleAddSecret(req: Request): Promise<Response> {
89
100
  );
90
101
  }
91
102
  upsertCredentialMetadata(service, field, {});
103
+ if (isManagedProxyCredential(service, field)) {
104
+ initializeProviders(getConfig());
105
+ }
92
106
  log.info({ service, field }, "Credential added via HTTP");
93
107
  return Response.json({ success: true, type, name }, { status: 201 });
94
108
  }
@@ -181,6 +195,9 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
181
195
  );
182
196
  }
183
197
  deleteCredentialMetadata(service, field);
198
+ if (isManagedProxyCredential(service, field)) {
199
+ initializeProviders(getConfig());
200
+ }
184
201
  log.info({ service, field }, "Credential deleted via HTTP");
185
202
  return Response.json({ success: true, type, name });
186
203
  }
@@ -29,7 +29,7 @@ import {
29
29
  generateAllowlistOptions,
30
30
  generateScopeOptions,
31
31
  } from "../../permissions/checker.js";
32
- import { getSecureKey } from "../../security/secure-keys.js";
32
+ import { getSecureKeyAsync } from "../../security/secure-keys.js";
33
33
  import { parseToolManifestFile } from "../../skills/tool-manifest.js";
34
34
  import {
35
35
  type ManifestOverride,
@@ -160,7 +160,7 @@ async function handleOAuthConnectStart(body: {
160
160
  const app = getApp(conn.oauthAppId);
161
161
  if (app) {
162
162
  clientId = app.clientId;
163
- clientSecret = getSecureKey(app.clientSecretCredentialPath);
163
+ clientSecret = await getSecureKeyAsync(app.clientSecretCredentialPath);
164
164
  }
165
165
  }
166
166
 
@@ -170,7 +170,7 @@ async function handleOAuthConnectStart(body: {
170
170
  if (dbApp) {
171
171
  clientId = dbApp.clientId;
172
172
  if (!clientSecret) {
173
- clientSecret = getSecureKey(dbApp.clientSecretCredentialPath);
173
+ clientSecret = await getSecureKeyAsync(dbApp.clientSecretCredentialPath);
174
174
  }
175
175
  }
176
176
  }
@@ -34,7 +34,9 @@ interface TreeEntry {
34
34
  function handleWorkspaceTree(ctx: RouteContext): Response {
35
35
  const requestedPath = ctx.url.searchParams.get("path") ?? "";
36
36
  const showHidden = ctx.url.searchParams.get("showHidden") === "true";
37
- const resolved = resolveWorkspacePath(requestedPath);
37
+ const resolved = resolveWorkspacePath(requestedPath, {
38
+ allowHidden: showHidden,
39
+ });
38
40
  if (resolved === undefined) {
39
41
  return httpError("BAD_REQUEST", "Invalid path", 400);
40
42
  }
@@ -96,7 +98,8 @@ function handleWorkspaceFile(ctx: RouteContext): Response {
96
98
  return httpError("BAD_REQUEST", "path query parameter is required", 400);
97
99
  }
98
100
 
99
- const resolved = resolveWorkspacePath(path);
101
+ const showHidden = ctx.url.searchParams.get("showHidden") === "true";
102
+ const resolved = resolveWorkspacePath(path, { allowHidden: showHidden });
100
103
  if (resolved === undefined) {
101
104
  return httpError("BAD_REQUEST", "Invalid path", 400);
102
105
  }
@@ -146,7 +149,8 @@ function handleWorkspaceFileContent(ctx: RouteContext): Response {
146
149
  );
147
150
  }
148
151
 
149
- const resolved = resolveWorkspacePath(path);
152
+ const showHidden = ctx.url.searchParams.get("showHidden") === "true";
153
+ const resolved = resolveWorkspacePath(path, { allowHidden: showHidden });
150
154
  if (resolved === undefined) {
151
155
  return httpError("BAD_REQUEST", "Invalid path", 400);
152
156
  }
@@ -7,10 +7,16 @@ import { getWorkspaceDir } from "../../util/platform.js";
7
7
  * Resolves a user-provided relative path to an absolute path within the workspace.
8
8
  * Returns the resolved absolute path, or undefined if the path escapes the workspace root.
9
9
  */
10
- export function resolveWorkspacePath(relativePath: string): string | undefined {
10
+ export function resolveWorkspacePath(
11
+ relativePath: string,
12
+ options?: { allowHidden?: boolean },
13
+ ): string | undefined {
11
14
  // Reject paths containing hidden (dot-prefixed) segments like .env, .git, .hidden/foo
12
15
  const segments = relativePath.split(/[/\\]/);
13
- if (segments.some((s) => s.startsWith(".") && s !== "." && s !== "..")) {
16
+ if (
17
+ !options?.allowHidden &&
18
+ segments.some((s) => s.startsWith(".") && s !== "." && s !== "..")
19
+ ) {
14
20
  return undefined;
15
21
  }
16
22
 
@@ -7,7 +7,7 @@ import {
7
7
  interface IntegrationProbe {
8
8
  name: string;
9
9
  category: string;
10
- isConnected: () => boolean;
10
+ isConnected: () => Promise<boolean>;
11
11
  }
12
12
 
13
13
  // Registry — add new integrations here:
@@ -15,7 +15,7 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
15
15
  {
16
16
  name: "Gmail",
17
17
  category: "email",
18
- isConnected: () => isProviderConnected("integration:gmail"),
18
+ isConnected: () => isProviderConnected("integration:google"),
19
19
  },
20
20
  {
21
21
  name: "Slack",
@@ -25,39 +25,46 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
25
25
  {
26
26
  name: "Twilio",
27
27
  category: "telephony",
28
- isConnected: () => hasTwilioCredentials(),
28
+ isConnected: async () => hasTwilioCredentials(),
29
29
  },
30
30
  {
31
31
  name: "Telegram",
32
32
  category: "messaging",
33
- isConnected: () => {
33
+ isConnected: async () => {
34
34
  const conn = getConnectionByProvider("telegram");
35
35
  return !!(conn && conn.status === "active");
36
36
  },
37
37
  },
38
38
  ];
39
39
 
40
- export function getIntegrationSummary(): Array<{
41
- name: string;
42
- category: string;
43
- connected: boolean;
44
- }> {
45
- return INTEGRATION_PROBES.map((probe) => ({
46
- name: probe.name,
47
- category: probe.category,
48
- connected: probe.isConnected(),
49
- }));
40
+ export async function getIntegrationSummary(): Promise<
41
+ Array<{
42
+ name: string;
43
+ category: string;
44
+ connected: boolean;
45
+ }>
46
+ > {
47
+ return Promise.all(
48
+ INTEGRATION_PROBES.map(async (probe) => ({
49
+ name: probe.name,
50
+ category: probe.category,
51
+ connected: await probe.isConnected(),
52
+ })),
53
+ );
50
54
  }
51
55
 
52
- export function formatIntegrationSummary(): string {
53
- const summary = getIntegrationSummary();
56
+ export async function formatIntegrationSummary(): Promise<string> {
57
+ const summary = await getIntegrationSummary();
54
58
  return summary
55
59
  .map((s) => `${s.name} ${s.connected ? "\u2713" : "\u2717"}`)
56
60
  .join(" | ");
57
61
  }
58
62
 
59
- export function hasCapability(category: string): boolean {
60
- return INTEGRATION_PROBES.some(
61
- (probe) => probe.category === category && probe.isConnected(),
63
+ export async function hasCapability(category: string): Promise<boolean> {
64
+ const results = await Promise.all(
65
+ INTEGRATION_PROBES.filter((probe) => probe.category === category).map(
66
+ (probe) => probe.isConnected(),
67
+ ),
62
68
  );
69
+ return results.some(Boolean);
63
70
  }
@@ -420,18 +420,17 @@ export interface OAuth2PreparedFlow {
420
420
  * URL directly in chat and the callback arrives asynchronously via the gateway.
421
421
  *
422
422
  * Supports two transports:
423
- * - **gateway** (default): routes callbacks through the public ingress URL.
424
- * Requires `ingress.publicBaseUrl` to be configured.
425
- * - **loopback**: starts a temporary localhost server to receive the callback.
426
- * Used for services like Slack that require pre-registered localhost redirect
427
- * URIs. The daemon is always local, so this works even in non-interactive
428
- * (channel) sessions.
423
+ * - **loopback** (default): starts a temporary localhost server to receive the
424
+ * callback. Works without any public URL or tunnel.
425
+ * - **gateway**: routes callbacks through the public ingress URL.
426
+ * Requires `ingress.publicBaseUrl` to be configured. Used for providers that
427
+ * don't support localhost redirects (e.g. Twitter, Notion).
429
428
  */
430
429
  export async function prepareOAuth2Flow(
431
430
  config: OAuth2Config,
432
431
  options?: OAuth2FlowOptions,
433
432
  ): Promise<OAuth2PreparedFlow> {
434
- const transport = options?.callbackTransport ?? "gateway";
433
+ const transport = options?.callbackTransport ?? "loopback";
435
434
 
436
435
  if (transport === "loopback") {
437
436
  return prepareLoopbackFlow(config, options?.loopbackPort);
@@ -3,7 +3,7 @@
3
3
  * available (macOS app embedded), with transparent fallback to the
4
4
  * encrypted-at-rest file store.
5
5
  *
6
- * Async variants try the broker first; sync variants always use the
6
+ * Async variants try the encrypted store first; sync variants always use the
7
7
  * encrypted store (startup code paths cannot do async I/O).
8
8
  */
9
9
 
@@ -82,30 +82,33 @@ export function isDowngradedFromKeychain(): boolean {
82
82
  }
83
83
 
84
84
  // ---------------------------------------------------------------------------
85
- // Async variants — try broker first, fall back to encrypted store
85
+ // Async variants — try encrypted store first, fall back to broker
86
86
  // ---------------------------------------------------------------------------
87
87
 
88
88
  /**
89
- * Async version of `getSecureKey`. When the broker is available it is
90
- * queried first. A `null` return from the broker means error (fall back
91
- * to encrypted store). A `{ found: false }` also falls back to the
92
- * encrypted store keys may exist only in `keys.enc` (e.g. written
93
- * while the broker was unavailable or via sync `setSecureKey`).
89
+ * Async version of `getSecureKey`. Checks the encrypted store first
90
+ * (instant) since `setSecureKeyAsync` always writes to both stores.
91
+ * Falls back to the broker for keys that may exist only in the macOS
92
+ * Keychain. Returns `undefined` if the key is not found in either store.
94
93
  */
95
94
  export async function getSecureKeyAsync(
96
95
  account: string,
97
96
  ): Promise<string | undefined> {
97
+ // Check encrypted store first (sync, instant). Since setSecureKeyAsync
98
+ // always writes to both broker and encrypted store, a hit here is
99
+ // authoritative and avoids the broker IPC round-trip.
100
+ const encResult = encryptedStore.getKey(account);
101
+ if (encResult != null && encResult.length > 0) return encResult;
102
+
103
+ // Not in encrypted store — try broker as fallback for keys that may
104
+ // exist only in the macOS Keychain (e.g. written by the app directly).
98
105
  const broker = getBroker();
99
106
  if (broker.isAvailable()) {
100
107
  const result = await broker.get(account);
101
- // null = broker error, fall back to encrypted store
102
- if (result == null) return encryptedStore.getKey(account);
103
- // Broker found the key — use it
104
- if (result.found) return result.value;
105
- // Broker says not found — check encrypted store as fallback
106
- return encryptedStore.getKey(account);
108
+ if (result?.found) return result.value;
107
109
  }
108
- return encryptedStore.getKey(account);
110
+
111
+ return undefined;
109
112
  }
110
113
 
111
114
  /**
@@ -115,7 +118,7 @@ export async function getSecureKeyAsync(
115
118
  *
116
119
  * If the broker is available but `broker.set()` fails we return `false`
117
120
  * immediately — falling through to an encrypted-store-only write would
118
- * leave the broker with stale data that async readers would still see.
121
+ * leave the two stores out of sync.
119
122
  */
120
123
  export async function setSecureKeyAsync(
121
124
  account: string,
@@ -161,7 +164,7 @@ export async function setSecureKeyAsync(
161
164
  *
162
165
  * If the broker is available but `broker.del()` fails we return `"error"`
163
166
  * immediately — falling through to an encrypted-store-only delete would
164
- * leave the broker with the key, and async readers would still see it.
167
+ * leave the broker with the key, causing stale reads on broker fallback.
165
168
  */
166
169
  export async function deleteSecureKeyAsync(
167
170
  account: string,
@@ -16,11 +16,15 @@ import {
16
16
  } from "../oauth/oauth-store.js";
17
17
  import { getLogger } from "../util/logger.js";
18
18
  import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
19
- import { getSecureKey, setSecureKeyAsync } from "./secure-keys.js";
19
+ import {
20
+ getSecureKey,
21
+ getSecureKeyAsync,
22
+ setSecureKeyAsync,
23
+ } from "./secure-keys.js";
20
24
 
21
25
  const log = getLogger("token-manager");
22
26
 
23
- const MESSAGING_SERVICES = new Set(["integration:gmail", "integration:slack"]);
27
+ const MESSAGING_SERVICES = new Set(["integration:google", "integration:slack"]);
24
28
 
25
29
  function recoveryHint(service: string): string {
26
30
  const shortName = service.startsWith("integration:")
@@ -185,7 +189,10 @@ interface RefreshConfig {
185
189
  * authMethod. Throws `TokenExpiredError` if the connection is not found
186
190
  * or incomplete.
187
191
  */
188
- function resolveRefreshConfig(service: string, connId: string): RefreshConfig {
192
+ async function resolveRefreshConfig(
193
+ service: string,
194
+ connId: string,
195
+ ): Promise<RefreshConfig> {
189
196
  const conn = getConnection(connId);
190
197
  if (!conn) {
191
198
  throw new TokenExpiredError(
@@ -219,9 +226,9 @@ function resolveRefreshConfig(service: string, connId: string): RefreshConfig {
219
226
  );
220
227
  }
221
228
 
222
- const secret = getSecureKey(app.clientSecretCredentialPath);
229
+ const secret = await getSecureKeyAsync(app.clientSecretCredentialPath);
223
230
 
224
- const refreshToken = getSecureKey(
231
+ const refreshToken = await getSecureKeyAsync(
225
232
  `oauth_connection/${conn.id}/refresh_token`,
226
233
  );
227
234
 
@@ -249,7 +256,7 @@ function resolveRefreshConfig(service: string, connId: string): RefreshConfig {
249
256
  * Throws `TokenExpiredError` if refresh is not possible.
250
257
  */
251
258
  async function doRefresh(service: string, connId: string): Promise<string> {
252
- const refreshConfig = resolveRefreshConfig(service, connId);
259
+ const refreshConfig = await resolveRefreshConfig(service, connId);
253
260
  const {
254
261
  tokenUrl,
255
262
  clientId: resolvedClientId,
@@ -55,27 +55,3 @@ export async function deployHtmlToVercel(opts: {
55
55
 
56
56
  return { url: publicUrl, deploymentId: data.id };
57
57
  }
58
-
59
- export async function deleteVercelDeployment(
60
- deploymentId: string,
61
- token: string,
62
- ): Promise<void> {
63
- const response = await fetch(
64
- `https://api.vercel.com/v13/deployments/${deploymentId}`,
65
- {
66
- method: "DELETE",
67
- headers: {
68
- Authorization: `Bearer ${token}`,
69
- },
70
- },
71
- );
72
-
73
- if (!response.ok) {
74
- const text = await response.text();
75
- throw new ProviderError(
76
- `Vercel delete deployment failed (${response.status}): ${text}`,
77
- "vercel",
78
- response.status,
79
- );
80
- }
81
- }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Handle confirmation decisions delivered via signal files from the CLI.
3
+ *
4
+ * The built-in CLI writes JSON to `signals/confirm` instead of making an
5
+ * HTTP POST to `/v1/confirm`. The daemon's ConfigWatcher detects the file
6
+ * change and invokes {@link handleConfirmationSignal}, which reads the
7
+ * payload and resolves the pending interaction in-process.
8
+ */
9
+
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ import type { UserDecision } from "../permissions/types.js";
14
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
15
+ import { getLogger } from "../util/logger.js";
16
+ import { getWorkspaceDir } from "../util/platform.js";
17
+
18
+ const log = getLogger("signal:confirm");
19
+
20
+ const VALID_DECISIONS: ReadonlySet<string> = new Set<string>([
21
+ "allow",
22
+ "allow_10m",
23
+ "allow_thread",
24
+ "always_allow",
25
+ "always_allow_high_risk",
26
+ "deny",
27
+ "always_deny",
28
+ "temporary_override",
29
+ ]);
30
+
31
+ function isUserDecision(value: string): value is UserDecision {
32
+ return VALID_DECISIONS.has(value);
33
+ }
34
+
35
+ /**
36
+ * Read the `signals/confirm` file and resolve the pending interaction.
37
+ * Called by ConfigWatcher when the signal file is written or modified.
38
+ */
39
+ export function handleConfirmationSignal(): void {
40
+ try {
41
+ const content = readFileSync(
42
+ join(getWorkspaceDir(), "signals", "confirm"),
43
+ "utf-8",
44
+ );
45
+ const parsed = JSON.parse(content) as {
46
+ requestId?: string;
47
+ decision?: string;
48
+ };
49
+ const { requestId, decision } = parsed;
50
+
51
+ if (!requestId || typeof requestId !== "string") {
52
+ log.warn("Confirmation signal missing requestId");
53
+ return;
54
+ }
55
+ if (!decision || !isUserDecision(decision)) {
56
+ log.warn({ decision }, "Confirmation signal has invalid decision");
57
+ return;
58
+ }
59
+
60
+ const interaction = pendingInteractions.resolve(requestId);
61
+ if (!interaction) {
62
+ log.warn({ requestId }, "No pending interaction for confirmation signal");
63
+ return;
64
+ }
65
+
66
+ interaction.session.handleConfirmationResponse(
67
+ requestId,
68
+ decision,
69
+ undefined,
70
+ undefined,
71
+ undefined,
72
+ { source: "button" },
73
+ );
74
+ log.info({ requestId, decision }, "Confirmation resolved via signal file");
75
+ } catch (err) {
76
+ log.error({ err }, "Failed to handle confirmation signal");
77
+ }
78
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Handle MCP reload signals from the CLI.
3
+ *
4
+ * When the CLI writes to `signals/mcp-reload`, the daemon's ConfigWatcher
5
+ * detects the file change and invokes {@link handleMcpReloadSignal} to
6
+ * restart MCP servers with the latest configuration.
7
+ */
8
+
9
+ import { reloadMcpServers } from "../daemon/mcp-reload-service.js";
10
+ import { getLogger } from "../util/logger.js";
11
+
12
+ const log = getLogger("signal:mcp-reload");
13
+
14
+ export function handleMcpReloadSignal(): void {
15
+ reloadMcpServers().catch((err: unknown) => {
16
+ log.error({ err }, "MCP reload triggered by signal file failed");
17
+ });
18
+ }
@@ -3,6 +3,7 @@ import { orchestrateOAuthConnect } from "../../oauth/connect-orchestrator.js";
3
3
  import {
4
4
  disconnectOAuthProvider,
5
5
  getAppByProviderAndClientId,
6
+ getConnectionByProviderAndAccount,
6
7
  getMostRecentAppByProvider,
7
8
  getProvider,
8
9
  } from "../../oauth/oauth-store.js";
@@ -19,7 +20,7 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
19
20
  import { credentialKey } from "../../security/credential-key.js";
20
21
  import {
21
22
  deleteSecureKeyAsync,
22
- getSecureKey,
23
+ getSecureKeyAsync,
23
24
  listSecureKeys,
24
25
  setSecureKeyAsync,
25
26
  } from "../../security/secure-keys.js";
@@ -72,6 +73,11 @@ class CredentialStoreTool implements Tool {
72
73
  type: "string",
73
74
  description: "Service name, e.g. gmail, github",
74
75
  },
76
+ account: {
77
+ type: "string",
78
+ description:
79
+ "Account identifier (e.g. email address) to target a specific connection when multiple accounts are connected for the same service. If omitted, uses the most recently connected account.",
80
+ },
75
81
  field: {
76
82
  type: "string",
77
83
  description: "Field name, e.g. password, username, recovery_email",
@@ -453,7 +459,19 @@ class CredentialStoreTool implements Tool {
453
459
  }
454
460
  // Also clean up any OAuth connection for this service (best-effort)
455
461
  try {
456
- const oauthResult = await disconnectOAuthProvider(service);
462
+ const accountHint = input.account as string | undefined;
463
+ let oauthResult: "disconnected" | "not-found" | "error";
464
+ if (accountHint) {
465
+ const targetConn = getConnectionByProviderAndAccount(
466
+ service,
467
+ accountHint,
468
+ );
469
+ oauthResult = targetConn
470
+ ? await disconnectOAuthProvider(service, undefined, targetConn.id)
471
+ : "not-found";
472
+ } else {
473
+ oauthResult = await disconnectOAuthProvider(service);
474
+ }
457
475
  if (oauthResult === "error") {
458
476
  log.warn(
459
477
  { service },
@@ -723,7 +741,7 @@ class CredentialStoreTool implements Tool {
723
741
  isError: true,
724
742
  };
725
743
 
726
- // Resolve aliases (e.g. "gmail" → "integration:gmail")
744
+ // Resolve aliases (e.g. "gmail" → "integration:google")
727
745
  const service = resolveService(rawService);
728
746
 
729
747
  // Code-side behavioral fields (identityVerifier, setup, etc.)
@@ -747,7 +765,7 @@ class CredentialStoreTool implements Tool {
747
765
  if (dbApp) {
748
766
  if (!clientId) clientId = dbApp.clientId;
749
767
  if (!clientSecret) {
750
- clientSecret = getSecureKey(dbApp.clientSecretCredentialPath);
768
+ clientSecret = await getSecureKeyAsync(dbApp.clientSecretCredentialPath);
751
769
  }
752
770
  }
753
771
  }
@@ -794,7 +812,6 @@ class CredentialStoreTool implements Tool {
794
812
  clientSecret,
795
813
  isInteractive: !!context.isInteractive,
796
814
  sendToClient: context.sendToClient,
797
- allowedTools: input.allowed_tools as string[] | undefined,
798
815
  ...(inputScopes ? { requestedScopes: inputScopes } : {}),
799
816
  onDeferredComplete: (deferredResult) => {
800
817
  // Emit oauth_connect_result to all connected SSE clients so the
@@ -19,7 +19,7 @@ import {
19
19
  routeConnection,
20
20
  stripQueryString,
21
21
  } from "../../../outbound-proxy/index.js";
22
- import { getSecureKey } from "../../../security/secure-keys.js";
22
+ import { getSecureKeyAsync } from "../../../security/secure-keys.js";
23
23
  import { getLogger } from "../../../util/logger.js";
24
24
  import { silentlyWithLog } from "../../../util/silently.js";
25
25
  import {
@@ -97,10 +97,10 @@ const acquireLocks = new Map<string, Promise<ProxySession>>();
97
97
  * Handles optional composition with a second credential and value transforms.
98
98
  * Returns null if any referenced credential cannot be resolved.
99
99
  */
100
- function buildInjectedValue(
100
+ async function buildInjectedValue(
101
101
  tpl: CredentialInjectionTemplate,
102
102
  primaryValue: string,
103
- ): string | null {
103
+ ): Promise<string | null> {
104
104
  let value = primaryValue;
105
105
 
106
106
  if (tpl.composeWith) {
@@ -109,7 +109,7 @@ function buildInjectedValue(
109
109
  tpl.composeWith.field,
110
110
  );
111
111
  if (!composed) return null;
112
- const composedValue = getSecureKey(composed.storageKey);
112
+ const composedValue = await getSecureKeyAsync(composed.storageKey);
113
113
  if (!composedValue) return null;
114
114
  value = `${value}${tpl.composeWith.separator}${composedValue}`;
115
115
  }
@@ -311,10 +311,10 @@ export async function startSession(
311
311
  if (tpl.injectionType === "header" && tpl.headerName) {
312
312
  const resolved = resolveById(credId);
313
313
  if (!resolved) return req.headers;
314
- const value = getSecureKey(resolved.storageKey);
314
+ const value = await getSecureKeyAsync(resolved.storageKey);
315
315
  if (!value) return req.headers;
316
316
 
317
- const headerValue = buildInjectedValue(tpl, value);
317
+ const headerValue = await buildInjectedValue(tpl, value);
318
318
  if (!headerValue) {
319
319
  log.warn(
320
320
  { host: req.hostname, credentialId: credId },
@@ -399,11 +399,11 @@ export async function startSession(
399
399
  const { credentialId, template } = decision;
400
400
  const resolved = resolveById(credentialId);
401
401
  if (!resolved) return {};
402
- const value = getSecureKey(resolved.storageKey);
402
+ const value = await getSecureKeyAsync(resolved.storageKey);
403
403
  if (!value) return {};
404
404
 
405
405
  if (template.injectionType === "header" && template.headerName) {
406
- const headerValue = buildInjectedValue(template, value);
406
+ const headerValue = await buildInjectedValue(template, value);
407
407
  if (!headerValue) {
408
408
  log.warn(
409
409
  { hostname, credentialId },
@@ -115,7 +115,7 @@ export async function executeScheduleCreate(
115
115
  });
116
116
 
117
117
  const fireDate = formatLocalDate(job.nextRunAt);
118
- const integrations = formatIntegrationSummary();
118
+ const integrations = await formatIntegrationSummary();
119
119
  return {
120
120
  content: [
121
121
  `One-shot schedule created successfully.`,
@@ -197,7 +197,7 @@ export async function executeScheduleCreate(
197
197
  : describeCronExpression(job.cronExpression);
198
198
 
199
199
  const nextRunDate = formatLocalDate(job.nextRunAt);
200
- const integrations = formatIntegrationSummary();
200
+ const integrations = await formatIntegrationSummary();
201
201
  return {
202
202
  content: [
203
203
  `Recurring schedule created successfully.`,
@@ -28,7 +28,7 @@ export interface WatcherProvider {
28
28
  id: string;
29
29
  /** Human-readable name. */
30
30
  displayName: string;
31
- /** Credential service required (e.g. 'integration:gmail'). */
31
+ /** Credential service required (e.g. 'integration:google'). */
32
32
  requiredCredentialService: string;
33
33
 
34
34
  /**
@@ -130,7 +130,7 @@ export const githubProvider: WatcherProvider = {
130
130
  _config: Record<string, unknown>,
131
131
  _watcherKey: string,
132
132
  ): Promise<FetchResult> {
133
- const connection = resolveOAuthConnection(credentialService);
133
+ const connection = await resolveOAuthConnection(credentialService);
134
134
  const since = watermark ?? new Date().toISOString();
135
135
  const items: WatcherItem[] = [];
136
136
  let page = 1;