@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
@@ -151,7 +151,7 @@ sequenceDiagram
151
151
 
152
152
  Note over UI,API: Tool Execution Flow
153
153
  Tool->>TokenMgr: withValidToken("gmail", callback)
154
- TokenMgr->>Store: getConnectionByProvider("integration:gmail")
154
+ TokenMgr->>Store: getConnectionByProvider("integration:google")
155
155
  TokenMgr->>Vault: getSecureKey("oauth_connection/{conn.id}/access_token")
156
156
  TokenMgr->>Store: check oauth_connections.expires_at
157
157
  alt Token expired
@@ -228,7 +228,7 @@ The OAuth extensibility layer makes adding a new OAuth provider a declarative op
228
228
 
229
229
  Protocol fields (`authUrl`, `tokenUrl`, `defaultScopes`, `scopePolicy`, `callbackTransport`) are stored in the `oauth_providers` database table rather than in code.
230
230
 
231
- Registered providers: `integration:gmail`, `integration:slack`, `integration:notion`. Short aliases (e.g. `gmail`, `slack`) are resolved via `resolveService()`.
231
+ Registered providers: `integration:google`, `integration:slack`, `integration:notion`. Short aliases (e.g. `gmail`, `slack`) are resolved via `resolveService()`.
232
232
 
233
233
  ### Scope Policy Engine
234
234
 
@@ -55,11 +55,11 @@ graph LR
55
55
 
56
56
  ### TypeScript side (runtime + gateway)
57
57
 
58
- | File | Role |
59
- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
- | `assistant/src/security/keychain-broker-client.ts` | Async UDS client for the runtime. Persistent socket connection, request/response correlation, auth token caching with auto-refresh on `UNAUTHORIZED`. Falls back gracefully (returns safe defaults, never throws). |
61
- | `assistant/src/security/secure-keys.ts` | Unified API surface. Sync variants use encrypted store only. Async variants (`getSecureKeyAsync`, `setSecureKeyAsync`, `deleteSecureKeyAsync`) try broker first. **Reads** fall back to the encrypted store when the broker is unavailable or key is not found. **Writes** return `false` on broker failure (no encrypted-store fallback). **Deletes** return `"deleted"`, `"not-found"`, or `"error"` to let callers distinguish idempotent no-ops from real failures. |
62
- | `gateway/src/credential-reader.ts` | Read-only credential reader. Tries broker via native async UDS connection (`node:net`), falls back to encrypted store. All public credential read functions are async. |
58
+ | File | Role |
59
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `assistant/src/security/keychain-broker-client.ts` | Async UDS client for the runtime. Persistent socket connection, request/response correlation, auth token caching with auto-refresh on `UNAUTHORIZED`. Falls back gracefully (returns safe defaults, never throws). |
61
+ | `assistant/src/security/secure-keys.ts` | Unified API surface. Sync variants use encrypted store only. Async variants (`getSecureKeyAsync`, `setSecureKeyAsync`, `deleteSecureKeyAsync`) check the encrypted store first for reads (instant), falling back to the broker for keys that may exist only in the macOS Keychain. **Writes** go to both stores; return `false` on broker failure (no encrypted-store fallback). **Deletes** return `"deleted"`, `"not-found"`, or `"error"` to let callers distinguish idempotent no-ops from real failures. |
62
+ | `gateway/src/credential-reader.ts` | Read-only credential reader. Tries broker via native async UDS connection (`node:net`), falls back to encrypted store. All public credential read functions are async. |
63
63
 
64
64
  ## Message Contract
65
65
 
@@ -181,6 +181,6 @@ Sync APIs are acceptable for startup paths (e.g. provider initialization, config
181
181
 
182
182
  ## Migration
183
183
 
184
- Existing encrypted store keys remain accessible — the encrypted store is always consulted as a **read** fallback when the broker does not have a key. Successful writes from async code paths go to both the broker (keychain) and the encrypted store, keeping both in sync. If a broker write or delete fails, the operation returns `false` without falling back to the encrypted store alone, preventing stale divergence. Callers must inspect the boolean return value and handle failures (typically by logging a warning). There is no one-time migration step required.
184
+ Existing encrypted store keys remain accessible — async reads check the encrypted store **first** (instant), falling back to the broker for keys that may exist only in the macOS Keychain. Successful writes from async code paths go to both the broker (keychain) and the encrypted store, keeping both in sync. If a broker write or delete fails, the operation returns `false` without falling back to the encrypted store alone, preventing stale divergence. Callers must inspect the boolean return value and handle failures (typically by logging a warning). There is no one-time migration step required.
185
185
 
186
186
  The old `keychain.ts` module (which called `/usr/bin/security` CLI directly) has been deleted. The old keychain-to-encrypted migration code has been removed. All keychain access now flows exclusively through the broker.
package/knip.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "entry": ["src/**/*.test.ts", "src/**/__tests__/**/*.ts", "scripts/**/*.ts"],
3
+ "project": ["src/**/*.ts", "src/**/*.tsx", "scripts/**/*.ts"],
4
+ "ignore": [
5
+ "src/browser-extension-relay/client.ts",
6
+ "src/contacts/index.ts",
7
+ "src/daemon/main.ts",
8
+ "src/daemon/tls-certs.ts",
9
+ "src/errors.ts",
10
+ "src/events/index.ts",
11
+ "src/followups/index.ts",
12
+ "src/playbooks/index.ts",
13
+ "src/runtime/auth/index.ts",
14
+ "src/tasks/candidate-store.ts",
15
+ "src/tools/browser/api-map.ts",
16
+ "src/tools/browser/auto-navigate.ts",
17
+ "src/tools/browser/headless-browser.ts",
18
+ "src/tools/browser/recording-store.ts",
19
+ "src/tools/tasks/index.ts"
20
+ ],
21
+ "ignoreDependencies": [
22
+ "@hono/node-server",
23
+ "@vellumai/cli",
24
+ "esbuild",
25
+ "hono",
26
+ "ink",
27
+ "preact",
28
+ "quicktype-core",
29
+ "tree-sitter-bash",
30
+ "typescript-json-schema"
31
+ ]
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.50",
3
+ "version": "0.4.51",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -16,13 +16,14 @@
16
16
  "format": "prettier --write .",
17
17
  "format:check": "prettier --check .",
18
18
  "lint": "eslint",
19
+ "lint:unused": "knip --include files,dependencies,unlisted",
19
20
  "typecheck": "bunx tsc --noEmit",
20
21
  "test": "bash scripts/test.sh",
21
22
  "test:coverage": "COVERAGE=true bash scripts/test.sh",
22
23
  "test:stable": "EXCLUDE_EXPERIMENTAL=true bash scripts/test.sh",
23
24
  "test:bench": "find src/__tests__ -maxdepth 1 -type f -name '*.benchmark.test.ts' -print0 | xargs -0 -P 1 -I {} bun test {}",
24
25
  "test:filesystem-tools": "bash scripts/test-filesystem-tools.sh",
25
- "postinstall": "cd .. && (git config core.hooksPath || git config core.hooksPath .githooks 2>/dev/null || true) && (bun run meta/feature-flags/sync-bundled-copies.ts 2>/dev/null || true)"
26
+ "postinstall": "cd .. && (git config core.hooksPath || git config core.hooksPath .githooks 2>/dev/null || true) && ([ -f meta/feature-flags/sync-bundled-copies.ts ] && bun run meta/feature-flags/sync-bundled-copies.ts 2>/dev/null || true)"
26
27
  },
27
28
  "dependencies": {
28
29
  "@anthropic-ai/claude-agent-sdk": "^0.2.42",
@@ -19,13 +19,21 @@ mock.module("../util/logger.js", () => ({
19
19
  }),
20
20
  }));
21
21
 
22
- const mockGetOrCreateConversation = mock((_key: string) => ({
23
- conversationId: "conv-test-123",
24
- created: false,
25
- }));
22
+ const mockGetConversationByKey = mock(
23
+ (_key: string): { conversationId: string } | null => ({
24
+ conversationId: "conv-test-123",
25
+ }),
26
+ );
26
27
 
27
28
  mock.module("../memory/conversation-key-store.js", () => ({
28
- getOrCreateConversation: mockGetOrCreateConversation,
29
+ getConversationByKey: mockGetConversationByKey,
30
+ // Ensure getOrCreateConversation is never called — BTW must not create
31
+ // persistent conversations.
32
+ getOrCreateConversation: () => {
33
+ throw new Error(
34
+ "getOrCreateConversation must not be called from btw-routes",
35
+ );
36
+ },
29
37
  }));
30
38
 
31
39
  const mockAddMessage = mock(() => {});
@@ -323,4 +331,52 @@ describe("POST /v1/btw", () => {
323
331
  // processing should still be false — the handler never sets it
324
332
  expect(session.processing).toBe(false);
325
333
  });
334
+
335
+ // -- No conversation creation (regression) --
336
+
337
+ test("unknown conversationKey does not create a DB conversation", async () => {
338
+ // Simulate a greeting request for a draft thread — no conversation exists.
339
+ mockGetConversationByKey.mockReturnValueOnce(null);
340
+
341
+ const session = makeMockSession();
342
+ const deps = makeSendMessageDeps(session);
343
+ const getOrCreateSessionSpy = deps.getOrCreateSession as ReturnType<
344
+ typeof mock
345
+ >;
346
+
347
+ const res = await callHandler(
348
+ { conversationKey: "greeting-abc123", content: "Generate a greeting" },
349
+ { sendMessageDeps: deps },
350
+ );
351
+ await readStream(res);
352
+
353
+ expect(res.status).toBe(200);
354
+
355
+ // Read-only lookup should be called
356
+ expect(mockGetConversationByKey).toHaveBeenCalledWith("greeting-abc123");
357
+
358
+ // Session should be created with the raw key (no DB conversation created)
359
+ expect(getOrCreateSessionSpy).toHaveBeenCalledWith("greeting-abc123");
360
+ });
361
+
362
+ test("known conversationKey resolves to existing conversation ID", async () => {
363
+ mockGetConversationByKey.mockReturnValueOnce({
364
+ conversationId: "existing-conv-id",
365
+ });
366
+
367
+ const session = makeMockSession();
368
+ const deps = makeSendMessageDeps(session);
369
+ const getOrCreateSessionSpy = deps.getOrCreateSession as ReturnType<
370
+ typeof mock
371
+ >;
372
+
373
+ const res = await callHandler(
374
+ { conversationKey: "my-thread-key", content: "What is 2+2?" },
375
+ { sendMessageDeps: deps },
376
+ );
377
+ await readStream(res);
378
+
379
+ expect(res.status).toBe(200);
380
+ expect(getOrCreateSessionSpy).toHaveBeenCalledWith("existing-conv-id");
381
+ });
326
382
  });
@@ -120,6 +120,14 @@ mock.module("../providers/registry.js", () => ({
120
120
  initializeProviders: () => {},
121
121
  }));
122
122
 
123
+ mock.module("../signals/mcp-reload.js", () => ({
124
+ handleMcpReloadSignal: () => {},
125
+ }));
126
+
127
+ mock.module("../signals/confirm.js", () => ({
128
+ handleConfirmationSignal: () => {},
129
+ }));
130
+
123
131
  let resetAllowlistCallCount = 0;
124
132
  mock.module("../security/secret-allowlist.js", () => ({
125
133
  resetAllowlist: () => {
@@ -236,6 +236,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
236
236
  "oauth/oauth-store.ts", // OAuth provider disconnect (delete stored tokens)
237
237
  "cli/commands/oauth/connections.ts", // CLI OAuth connection delete (legacy credential cleanup)
238
238
  "oauth/manual-token-connection.ts", // manual-token provider backfill (keychain credential existence check)
239
+ "cli/commands/doctor.ts", // CLI diagnostic API key verification via secure storage
239
240
  ]);
240
241
 
241
242
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -504,7 +505,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
504
505
 
505
506
  test("upsertCredentialMetadata does not accept oauth2ClientSecret or other OAuth fields", () => {
506
507
  const record = upsertCredentialMetadata(
507
- "integration:gmail",
508
+ "integration:google",
508
509
  "access_token",
509
510
  {
510
511
  allowedTools: ["api_request"],
@@ -517,14 +518,14 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
517
518
 
518
519
  test("client secret is read from secure store, not metadata", () => {
519
520
  setSecureKey(
520
- credentialKey("integration:gmail", "client_secret"),
521
+ credentialKey("integration:google", "client_secret"),
521
522
  "my-secret",
522
523
  );
523
- upsertCredentialMetadata("integration:gmail", "access_token", {
524
+ upsertCredentialMetadata("integration:google", "access_token", {
524
525
  allowedTools: ["api_request"],
525
526
  });
526
527
 
527
- const meta = getCredentialMetadata("integration:gmail", "access_token");
528
+ const meta = getCredentialMetadata("integration:google", "access_token");
528
529
  expect(meta).toBeDefined();
529
530
  expect("oauth2ClientSecret" in meta!).toBe(false);
530
531
  // OAuth-specific fields are no longer in metadata (v5)
@@ -533,7 +534,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
533
534
 
534
535
  // Secret is in secure store
535
536
  expect(
536
- getSecureKey(credentialKey("integration:gmail", "client_secret")),
537
+ getSecureKey(credentialKey("integration:google", "client_secret")),
537
538
  ).toBe("my-secret");
538
539
  });
539
540
 
@@ -543,7 +544,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
543
544
  credentials: [
544
545
  {
545
546
  credentialId: "cred-v2-secret",
546
- service: "integration:gmail",
547
+ service: "integration:google",
547
548
  field: "access_token",
548
549
  allowedTools: [],
549
550
  allowedDomains: [],
@@ -561,7 +562,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
561
562
  "utf-8",
562
563
  );
563
564
 
564
- const meta = getCredentialMetadata("integration:gmail", "access_token");
565
+ const meta = getCredentialMetadata("integration:google", "access_token");
565
566
  expect(meta).toBeDefined();
566
567
  expect("oauth2ClientSecret" in meta!).toBe(false);
567
568
 
@@ -531,8 +531,8 @@ describe("credential_store tool — prompt action", () => {
531
531
  describe("credential_store tool — oauth2_connect error paths", () => {
532
532
  /** Well-known provider rows returned by the mocked getProvider */
533
533
  const wellKnownProviders: Record<string, object> = {
534
- "integration:gmail": {
535
- key: "integration:gmail",
534
+ "integration:google": {
535
+ key: "integration:google",
536
536
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
537
537
  tokenUrl: "https://oauth2.googleapis.com/token",
538
538
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -624,9 +624,10 @@ describe("credential_store tool — oauth2_connect error paths", () => {
624
624
  expect(result.content).toContain("client_id is required");
625
625
  });
626
626
 
627
- test("requires interactive context", async () => {
628
- // Register custom-svc as a provider so the orchestrator finds it
629
- // and reaches the non-interactive check (gateway transport).
627
+ test("non-interactive loopback oauth2_connect returns deferred auth URL", async () => {
628
+ // After the blanket non-interactive guard was removed (#16337),
629
+ // loopback-transport flows succeed with a deferred auth URL so the
630
+ // agent can present it to the user.
630
631
  mockGetProvider.mockImplementation((key: string) => {
631
632
  if (key === "custom-svc") {
632
633
  return {
@@ -651,11 +652,11 @@ describe("credential_store tool — oauth2_connect error paths", () => {
651
652
  },
652
653
  { ..._ctx, isInteractive: false },
653
654
  );
654
- expect(result.isError).toBe(true);
655
- expect(result.content).toContain("non-interactive session");
655
+ expect(result.isError).toBe(false);
656
+ expect(result.content).toContain("mock-auth-url.example.com");
656
657
  });
657
658
 
658
- test("resolves gmail alias to integration:gmail", async () => {
659
+ test("resolves gmail alias to integration:google", async () => {
659
660
  // Even with alias resolution, missing client_id should still fail
660
661
  const result = await credentialStoreTool.execute(
661
662
  {
@@ -687,13 +688,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
687
688
  // and store client_secret in the secure store.
688
689
  mockGetMostRecentAppByProvider.mockImplementation(() => ({
689
690
  id: "test-app-id",
690
- providerKey: "integration:gmail",
691
+ providerKey: "integration:google",
691
692
  clientId: "stored-client-id-123",
692
693
  clientSecretCredentialPath: "oauth_app/test-app-id/client_secret",
693
694
  createdAt: Date.now(),
694
695
  }));
695
696
  mockGetProvider.mockImplementation(() => ({
696
- key: "integration:gmail",
697
+ key: "integration:google",
697
698
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
698
699
  tokenUrl: "https://oauth2.googleapis.com/token",
699
700
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -731,12 +732,12 @@ describe("credential_store tool — oauth2_connect error paths", () => {
731
732
  mockGetAppByProviderAndClientId.mockImplementation(
732
733
  (providerKey: string, cId: string) => {
733
734
  if (
734
- providerKey === "integration:gmail" &&
735
+ providerKey === "integration:google" &&
735
736
  cId === "caller-supplied-client-id"
736
737
  ) {
737
738
  return {
738
739
  id: "matched-app-id",
739
- providerKey: "integration:gmail",
740
+ providerKey: "integration:google",
740
741
  clientId: "caller-supplied-client-id",
741
742
  clientSecretCredentialPath:
742
743
  "oauth_app/matched-app-id/client_secret",
@@ -747,7 +748,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
747
748
  },
748
749
  );
749
750
  mockGetProvider.mockImplementation(() => ({
750
- key: "integration:gmail",
751
+ key: "integration:google",
751
752
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
752
753
  tokenUrl: "https://oauth2.googleapis.com/token",
753
754
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -782,13 +783,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
782
783
  // use getMostRecentAppByProvider (the fallback heuristic).
783
784
  mockGetMostRecentAppByProvider.mockImplementation(() => ({
784
785
  id: "recent-app-id",
785
- providerKey: "integration:gmail",
786
+ providerKey: "integration:google",
786
787
  clientId: "recent-client-id",
787
788
  clientSecretCredentialPath: "oauth_app/recent-app-id/client_secret",
788
789
  createdAt: Date.now(),
789
790
  }));
790
791
  mockGetProvider.mockImplementation(() => ({
791
- key: "integration:gmail",
792
+ key: "integration:google",
792
793
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
793
794
  tokenUrl: "https://oauth2.googleapis.com/token",
794
795
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -822,7 +823,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
822
823
  // report the missing secret error.
823
824
  mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
824
825
  mockGetProvider.mockImplementation(() => ({
825
- key: "integration:gmail",
826
+ key: "integration:google",
826
827
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
827
828
  tokenUrl: "https://oauth2.googleapis.com/token",
828
829
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -853,12 +854,12 @@ describe("credential_store tool — oauth2_connect error paths", () => {
853
854
  // guardrail.
854
855
  mockGetMostRecentAppByProvider.mockImplementation(() => ({
855
856
  id: "test-app-id-no-secret",
856
- providerKey: "integration:gmail",
857
+ providerKey: "integration:google",
857
858
  clientId: "stored-client-id-456",
858
859
  createdAt: Date.now(),
859
860
  }));
860
861
  mockGetProvider.mockImplementation(() => ({
861
- key: "integration:gmail",
862
+ key: "integration:google",
862
863
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
863
864
  tokenUrl: "https://oauth2.googleapis.com/token",
864
865
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -735,7 +735,7 @@ describe("credential_store tool", () => {
735
735
  await credentialStoreTool.execute(
736
736
  {
737
737
  action: "store",
738
- service: "integration:gmail",
738
+ service: "integration:google",
739
739
  field: "api_key",
740
740
  value: "test-value",
741
741
  },
@@ -743,9 +743,9 @@ describe("credential_store tool", () => {
743
743
  );
744
744
 
745
745
  // Simulate an active OAuth connection for this service
746
- mockConnections.set("integration:gmail", {
746
+ mockConnections.set("integration:google", {
747
747
  id: "conn-gmail",
748
- providerKey: "integration:gmail",
748
+ providerKey: "integration:google",
749
749
  oauthAppId: "app-gmail",
750
750
  expiresAt: Date.now() + 3600_000,
751
751
  });
@@ -753,7 +753,7 @@ describe("credential_store tool", () => {
753
753
  const result = await credentialStoreTool.execute(
754
754
  {
755
755
  action: "delete",
756
- service: "integration:gmail",
756
+ service: "integration:google",
757
757
  field: "api_key",
758
758
  },
759
759
  _ctx,
@@ -764,7 +764,7 @@ describe("credential_store tool", () => {
764
764
  // Verify disconnectOAuthProvider was called with the service name
765
765
  expect(mockDisconnectOAuthProvider).toHaveBeenCalledTimes(1);
766
766
  expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith(
767
- "integration:gmail",
767
+ "integration:google",
768
768
  );
769
769
  });
770
770
  });
@@ -1343,7 +1343,7 @@ describe("withValidToken refresh deduplication", () => {
1343
1343
  }
1344
1344
 
1345
1345
  test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
1346
- setupService("integration:gmail");
1346
+ setupService("integration:google");
1347
1347
 
1348
1348
  let resolveRefresh!: (value: {
1349
1349
  accessToken: string;
@@ -1367,9 +1367,9 @@ describe("withValidToken refresh deduplication", () => {
1367
1367
 
1368
1368
  // Launch 3 concurrent withValidToken calls — all will get a non-expired
1369
1369
  // token first, call the callback, get a 401, and then try to refresh.
1370
- const p1 = withValidToken("integration:gmail", callback);
1371
- const p2 = withValidToken("integration:gmail", callback);
1372
- const p3 = withValidToken("integration:gmail", callback);
1370
+ const p1 = withValidToken("integration:google", callback);
1371
+ const p2 = withValidToken("integration:google", callback);
1372
+ const p3 = withValidToken("integration:google", callback);
1373
1373
 
1374
1374
  // Let the event loop tick so all 3 calls enter the 401 retry path
1375
1375
  await new Promise((r) => setTimeout(r, 10));
@@ -1391,7 +1391,7 @@ describe("withValidToken refresh deduplication", () => {
1391
1391
  });
1392
1392
 
1393
1393
  test("concurrent refreshes for different services proceed independently", async () => {
1394
- setupService("integration:gmail");
1394
+ setupService("integration:google");
1395
1395
  setupService("integration:slack");
1396
1396
 
1397
1397
  let resolveGmail!: (value: {
@@ -1436,7 +1436,7 @@ describe("withValidToken refresh deduplication", () => {
1436
1436
  return `slack-${token}`;
1437
1437
  };
1438
1438
 
1439
- const p1 = withValidToken("integration:gmail", gmailCallback);
1439
+ const p1 = withValidToken("integration:google", gmailCallback);
1440
1440
  const p2 = withValidToken("integration:slack", slackCallback);
1441
1441
 
1442
1442
  await new Promise((r) => setTimeout(r, 10));
@@ -1455,7 +1455,7 @@ describe("withValidToken refresh deduplication", () => {
1455
1455
  });
1456
1456
 
1457
1457
  test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
1458
- setupService("integration:gmail");
1458
+ setupService("integration:google");
1459
1459
 
1460
1460
  let refreshCount = 0;
1461
1461
  mockRefreshOAuth2Token.mockImplementation(() => {
@@ -1470,7 +1470,7 @@ describe("withValidToken refresh deduplication", () => {
1470
1470
 
1471
1471
  // First call triggers a refresh (old token → 401 → refresh → token-1)
1472
1472
  const r1 = await withValidToken(
1473
- "integration:gmail",
1473
+ "integration:google",
1474
1474
  async (token: string) => {
1475
1475
  if (token !== "token-1") throw err401;
1476
1476
  return token;
@@ -1482,7 +1482,7 @@ describe("withValidToken refresh deduplication", () => {
1482
1482
  // Second call also triggers a 401 to verify dedup state was cleaned up
1483
1483
  // and a new refresh is allowed (not deduplicated with the first).
1484
1484
  const r2 = await withValidToken(
1485
- "integration:gmail",
1485
+ "integration:google",
1486
1486
  async (token: string) => {
1487
1487
  if (token !== "token-2") throw err401;
1488
1488
  return token;
@@ -1495,7 +1495,7 @@ describe("withValidToken refresh deduplication", () => {
1495
1495
  });
1496
1496
 
1497
1497
  test("deduplication propagates refresh errors to all waiting callers", async () => {
1498
- setupService("integration:gmail");
1498
+ setupService("integration:google");
1499
1499
 
1500
1500
  mockRefreshOAuth2Token.mockImplementation(() =>
1501
1501
  Promise.reject(
@@ -1513,8 +1513,8 @@ describe("withValidToken refresh deduplication", () => {
1513
1513
  };
1514
1514
 
1515
1515
  // Launch 2 concurrent calls — both should fail with the same error
1516
- const p1 = withValidToken("integration:gmail", callback);
1517
- const p2 = withValidToken("integration:gmail", callback);
1516
+ const p1 = withValidToken("integration:google", callback);
1517
+ const p2 = withValidToken("integration:google", callback);
1518
1518
 
1519
1519
  const results = await Promise.allSettled([p1, p2]);
1520
1520