@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
@@ -46,6 +46,16 @@ let mockUpsertAppResult: Record<string, unknown> = {
46
46
  createdAt: 1700000000000,
47
47
  updatedAt: 1700000000000,
48
48
  };
49
+ let mockUpsertAppImpl:
50
+ | ((
51
+ provider: string,
52
+ clientId: string,
53
+ clientSecretOpts?: {
54
+ clientSecretValue?: string;
55
+ clientSecretCredentialPath?: string;
56
+ },
57
+ ) => Promise<Record<string, unknown>>)
58
+ | undefined;
49
59
 
50
60
  // Connect mock state
51
61
  let mockOrchestrateOAuthConnect: (
@@ -65,6 +75,10 @@ let mockGetProviderBehavior: (
65
75
  providerKey: string,
66
76
  ) => Record<string, unknown> | undefined = () => undefined;
67
77
  let mockGetSecureKey: (account: string) => string | undefined = () => undefined;
78
+ let mockGetCredentialMetadata: (
79
+ service: string,
80
+ field: string,
81
+ ) => Record<string, unknown> | undefined = () => undefined;
68
82
 
69
83
  function nextUUID(): string {
70
84
  idCounter += 1;
@@ -116,6 +130,9 @@ mock.module("../oauth/oauth-store.js", () => ({
116
130
  clientSecretCredentialPath?: string;
117
131
  },
118
132
  ) => {
133
+ if (mockUpsertAppImpl) {
134
+ return mockUpsertAppImpl(provider, clientId, clientSecretOpts);
135
+ }
119
136
  mockUpsertAppCalls.push({ provider, clientId, clientSecretOpts });
120
137
  return mockUpsertAppResult;
121
138
  },
@@ -164,7 +181,8 @@ mock.module("../security/secure-keys.js", () => ({
164
181
 
165
182
  mock.module("../tools/credentials/metadata-store.js", () => ({
166
183
  assertMetadataWritable: () => {},
167
- getCredentialMetadata: () => undefined,
184
+ getCredentialMetadata: (service: string, field: string) =>
185
+ mockGetCredentialMetadata(service, field),
168
186
  upsertCredentialMetadata: () => ({}),
169
187
  listCredentialMetadata: () => [],
170
188
  deleteCredentialMetadata: (service: string, field: string): boolean => {
@@ -319,11 +337,11 @@ describe("assistant oauth connections token <provider-key>", () => {
319
337
  const { exitCode, stdout } = await runCli([
320
338
  "connections",
321
339
  "token",
322
- "integration:gmail",
340
+ "integration:google",
323
341
  ]);
324
342
  expect(exitCode).toBe(0);
325
343
  expect(stdout).toBe("gmail-token\n");
326
- expect(capturedService).toBe("integration:gmail");
344
+ expect(capturedService).toBe("integration:google");
327
345
  });
328
346
 
329
347
  test("exits 1 when no token exists", async () => {
@@ -403,30 +421,30 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
403
421
  const result = await runCli([
404
422
  "connections",
405
423
  "disconnect",
406
- "integration:gmail",
424
+ "integration:google",
407
425
  "--json",
408
426
  ]);
409
427
  expect(result.exitCode).toBe(0);
410
428
  const parsed = JSON.parse(result.stdout);
411
429
  expect(parsed.ok).toBe(true);
412
- expect(parsed.service).toBe("integration:gmail");
430
+ expect(parsed.service).toBe("integration:google");
413
431
 
414
432
  // disconnectOAuthProvider should have been called with the full provider key
415
- expect(disconnectOAuthProviderCalls).toEqual(["integration:gmail"]);
433
+ expect(disconnectOAuthProviderCalls).toEqual(["integration:google"]);
416
434
  });
417
435
 
418
436
  test("reports not-found when nothing exists", async () => {
419
437
  const result = await runCli([
420
438
  "connections",
421
439
  "disconnect",
422
- "integration:gmail",
440
+ "integration:google",
423
441
  "--json",
424
442
  ]);
425
443
  expect(result.exitCode).toBe(1);
426
444
  const parsed = JSON.parse(result.stdout);
427
445
  expect(parsed.ok).toBe(false);
428
446
  expect(parsed.error).toContain("No OAuth connection or credentials");
429
- expect(parsed.error).toContain("integration:gmail");
447
+ expect(parsed.error).toContain("integration:google");
430
448
  });
431
449
 
432
450
  test("cleans up legacy credential keys if present", async () => {
@@ -439,12 +457,12 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
439
457
  ];
440
458
  for (const field of legacyFields) {
441
459
  secureKeyStore.set(
442
- credentialKey("integration:gmail", field),
460
+ credentialKey("integration:google", field),
443
461
  `legacy_${field}_value`,
444
462
  );
445
463
  metadataStore.push({
446
464
  credentialId: nextUUID(),
447
- service: "integration:gmail",
465
+ service: "integration:google",
448
466
  field,
449
467
  allowedTools: [],
450
468
  allowedDomains: [],
@@ -456,22 +474,22 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
456
474
  const result = await runCli([
457
475
  "connections",
458
476
  "disconnect",
459
- "integration:gmail",
477
+ "integration:google",
460
478
  "--json",
461
479
  ]);
462
480
  expect(result.exitCode).toBe(0);
463
481
  const parsed = JSON.parse(result.stdout);
464
482
  expect(parsed.ok).toBe(true);
465
- expect(parsed.service).toBe("integration:gmail");
483
+ expect(parsed.service).toBe("integration:google");
466
484
 
467
485
  // All legacy keys should be removed
468
486
  for (const field of legacyFields) {
469
487
  expect(
470
- secureKeyStore.has(credentialKey("integration:gmail", field)),
488
+ secureKeyStore.has(credentialKey("integration:google", field)),
471
489
  ).toBe(false);
472
490
  expect(
473
491
  metadataStore.find(
474
- (m) => m.service === "integration:gmail" && m.field === field,
492
+ (m) => m.service === "integration:google" && m.field === field,
475
493
  ),
476
494
  ).toBeUndefined();
477
495
  }
@@ -483,12 +501,12 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
483
501
 
484
502
  // Seed a legacy credential key
485
503
  secureKeyStore.set(
486
- credentialKey("integration:gmail", "access_token"),
504
+ credentialKey("integration:google", "access_token"),
487
505
  "legacy_token",
488
506
  );
489
507
  metadataStore.push({
490
508
  credentialId: nextUUID(),
491
- service: "integration:gmail",
509
+ service: "integration:google",
492
510
  field: "access_token",
493
511
  allowedTools: [],
494
512
  allowedDomains: [],
@@ -499,7 +517,7 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
499
517
  const result = await runCli([
500
518
  "connections",
501
519
  "disconnect",
502
- "integration:gmail",
520
+ "integration:google",
503
521
  "--json",
504
522
  ]);
505
523
  expect(result.exitCode).toBe(0);
@@ -507,9 +525,9 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
507
525
  expect(parsed.ok).toBe(true);
508
526
 
509
527
  // Both should be cleaned up
510
- expect(disconnectOAuthProviderCalls).toEqual(["integration:gmail"]);
528
+ expect(disconnectOAuthProviderCalls).toEqual(["integration:google"]);
511
529
  expect(
512
- secureKeyStore.has(credentialKey("integration:gmail", "access_token")),
530
+ secureKeyStore.has(credentialKey("integration:google", "access_token")),
513
531
  ).toBe(false);
514
532
  });
515
533
  });
@@ -521,7 +539,7 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
521
539
  describe("assistant oauth providers list", () => {
522
540
  const fakeProviders = [
523
541
  {
524
- providerKey: "integration:gmail",
542
+ providerKey: "integration:google",
525
543
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
526
544
  tokenUrl: "https://oauth2.googleapis.com/token",
527
545
  defaultScopes: "[]",
@@ -578,7 +596,7 @@ describe("assistant oauth providers list", () => {
578
596
  const parsed = JSON.parse(stdout);
579
597
  expect(parsed).toHaveLength(4);
580
598
  const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
581
- expect(keys).toContain("integration:gmail");
599
+ expect(keys).toContain("integration:google");
582
600
  expect(keys).toContain("integration:google-calendar");
583
601
  expect(keys).toContain("integration:slack");
584
602
  expect(keys).toContain("integration:twitter");
@@ -589,13 +607,13 @@ describe("assistant oauth providers list", () => {
589
607
  "providers",
590
608
  "list",
591
609
  "--provider-key",
592
- "gmail",
610
+ "slack",
593
611
  "--json",
594
612
  ]);
595
613
  expect(exitCode).toBe(0);
596
614
  const parsed = JSON.parse(stdout);
597
615
  expect(parsed).toHaveLength(1);
598
- expect(parsed[0].providerKey).toBe("integration:gmail");
616
+ expect(parsed[0].providerKey).toBe("integration:slack");
599
617
  });
600
618
 
601
619
  test("filters by comma-separated OR values", async () => {
@@ -603,15 +621,16 @@ describe("assistant oauth providers list", () => {
603
621
  "providers",
604
622
  "list",
605
623
  "--provider-key",
606
- "gmail,google",
624
+ "slack,google",
607
625
  "--json",
608
626
  ]);
609
627
  expect(exitCode).toBe(0);
610
628
  const parsed = JSON.parse(stdout);
611
- expect(parsed).toHaveLength(2);
629
+ expect(parsed).toHaveLength(3);
612
630
  const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
613
- expect(keys).toContain("integration:gmail");
631
+ expect(keys).toContain("integration:google");
614
632
  expect(keys).toContain("integration:google-calendar");
633
+ expect(keys).toContain("integration:slack");
615
634
  });
616
635
 
617
636
  test("returns empty array when comma-separated filter has no matches", async () => {
@@ -632,15 +651,16 @@ describe("assistant oauth providers list", () => {
632
651
  "providers",
633
652
  "list",
634
653
  "--provider-key",
635
- "gmail, google",
654
+ "slack, google",
636
655
  "--json",
637
656
  ]);
638
657
  expect(exitCode).toBe(0);
639
658
  const parsed = JSON.parse(stdout);
640
- expect(parsed).toHaveLength(2);
659
+ expect(parsed).toHaveLength(3);
641
660
  const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
642
- expect(keys).toContain("integration:gmail");
661
+ expect(keys).toContain("integration:google");
643
662
  expect(keys).toContain("integration:google-calendar");
663
+ expect(keys).toContain("integration:slack");
644
664
  });
645
665
 
646
666
  test("ignores empty segments from extra commas in --provider-key", async () => {
@@ -648,15 +668,16 @@ describe("assistant oauth providers list", () => {
648
668
  "providers",
649
669
  "list",
650
670
  "--provider-key",
651
- "gmail,,google",
671
+ "slack,,google",
652
672
  "--json",
653
673
  ]);
654
674
  expect(exitCode).toBe(0);
655
675
  const parsed = JSON.parse(stdout);
656
- expect(parsed).toHaveLength(2);
676
+ expect(parsed).toHaveLength(3);
657
677
  const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
658
- expect(keys).toContain("integration:gmail");
678
+ expect(keys).toContain("integration:google");
659
679
  expect(keys).toContain("integration:google-calendar");
680
+ expect(keys).toContain("integration:slack");
660
681
  });
661
682
  });
662
683
 
@@ -685,6 +706,14 @@ describe("assistant oauth connections connect <provider-key>", () => {
685
706
  });
686
707
 
687
708
  test("completes interactive flow and prints success (human mode)", async () => {
709
+ mockGetAppByProviderAndClientId = () => ({
710
+ id: "app-1",
711
+ clientId: "test-id",
712
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
713
+ providerKey: "integration:google",
714
+ createdAt: 0,
715
+ updatedAt: 0,
716
+ });
688
717
  mockOrchestrateOAuthConnect = async () => ({
689
718
  success: true,
690
719
  deferred: false,
@@ -695,7 +724,7 @@ describe("assistant oauth connections connect <provider-key>", () => {
695
724
  const { exitCode, stdout } = await runCli([
696
725
  "connections",
697
726
  "connect",
698
- "integration:gmail",
727
+ "integration:google",
699
728
  "--client-id",
700
729
  "test-id",
701
730
  ]);
@@ -704,6 +733,14 @@ describe("assistant oauth connections connect <provider-key>", () => {
704
733
  });
705
734
 
706
735
  test("completes interactive flow and returns JSON with --json flag", async () => {
736
+ mockGetAppByProviderAndClientId = () => ({
737
+ id: "app-1",
738
+ clientId: "test-id",
739
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
740
+ providerKey: "integration:google",
741
+ createdAt: 0,
742
+ updatedAt: 0,
743
+ });
707
744
  mockOrchestrateOAuthConnect = async () => ({
708
745
  success: true,
709
746
  deferred: false,
@@ -714,7 +751,7 @@ describe("assistant oauth connections connect <provider-key>", () => {
714
751
  const { exitCode, stdout } = await runCli([
715
752
  "connections",
716
753
  "connect",
717
- "integration:gmail",
754
+ "integration:google",
718
755
  "--client-id",
719
756
  "test-id",
720
757
  "--json",
@@ -729,18 +766,26 @@ describe("assistant oauth connections connect <provider-key>", () => {
729
766
  });
730
767
 
731
768
  test("returns auth URL in default (non-interactive) mode (JSON)", async () => {
769
+ mockGetAppByProviderAndClientId = () => ({
770
+ id: "app-1",
771
+ clientId: "test-id",
772
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
773
+ providerKey: "integration:google",
774
+ createdAt: 0,
775
+ updatedAt: 0,
776
+ });
732
777
  mockOrchestrateOAuthConnect = async () => ({
733
778
  success: true,
734
779
  deferred: true,
735
780
  authUrl: "https://example.com/auth",
736
781
  state: "abc",
737
- service: "integration:gmail",
782
+ service: "integration:google",
738
783
  });
739
784
 
740
785
  const { exitCode, stdout } = await runCli([
741
786
  "connections",
742
787
  "connect",
743
- "integration:gmail",
788
+ "integration:google",
744
789
  "--client-id",
745
790
  "test-id",
746
791
  "--json",
@@ -758,7 +803,7 @@ describe("assistant oauth connections connect <provider-key>", () => {
758
803
  const { exitCode, stdout } = await runCli([
759
804
  "connections",
760
805
  "connect",
761
- "integration:gmail",
806
+ "integration:google",
762
807
  "--json",
763
808
  ]);
764
809
  expect(exitCode).toBe(1);
@@ -772,7 +817,7 @@ describe("assistant oauth connections connect <provider-key>", () => {
772
817
  id: "app-1",
773
818
  clientId: "db-client-id",
774
819
  clientSecretCredentialPath: "oauth_app/app-1/client_secret",
775
- providerKey: "integration:gmail",
820
+ providerKey: "integration:google",
776
821
  createdAt: 0,
777
822
  updatedAt: 0,
778
823
  });
@@ -787,7 +832,7 @@ describe("assistant oauth connections connect <provider-key>", () => {
787
832
  };
788
833
  };
789
834
 
790
- await runCli(["connections", "connect", "integration:gmail"]);
835
+ await runCli(["connections", "connect", "integration:google"]);
791
836
  expect(capturedClientId).toBe("db-client-id");
792
837
  });
793
838
 
@@ -796,7 +841,7 @@ describe("assistant oauth connections connect <provider-key>", () => {
796
841
  id: "app-1",
797
842
  clientId: "db-client-id",
798
843
  clientSecretCredentialPath: "oauth_app/app-1/client_secret",
799
- providerKey: "integration:gmail",
844
+ providerKey: "integration:google",
800
845
  createdAt: 0,
801
846
  updatedAt: 0,
802
847
  });
@@ -814,13 +859,21 @@ describe("assistant oauth connections connect <provider-key>", () => {
814
859
  };
815
860
  };
816
861
 
817
- await runCli(["connections", "connect", "integration:gmail"]);
862
+ await runCli(["connections", "connect", "integration:google"]);
818
863
  expect(capturedOpts).toBeDefined();
819
864
  expect(capturedOpts!.clientId).toBe("db-client-id");
820
865
  expect(capturedOpts!.clientSecret).toBe("db-secret");
821
866
  });
822
867
 
823
868
  test("outputs error from orchestrator", async () => {
869
+ mockGetAppByProviderAndClientId = () => ({
870
+ id: "app-1",
871
+ clientId: "x",
872
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
873
+ providerKey: "integration:google",
874
+ createdAt: 0,
875
+ updatedAt: 0,
876
+ });
824
877
  mockOrchestrateOAuthConnect = async () => ({
825
878
  success: false,
826
879
  error: "Something went wrong",
@@ -829,7 +882,7 @@ describe("assistant oauth connections connect <provider-key>", () => {
829
882
  const { exitCode, stdout } = await runCli([
830
883
  "connections",
831
884
  "connect",
832
- "integration:gmail",
885
+ "integration:google",
833
886
  "--client-id",
834
887
  "x",
835
888
  "--json",
@@ -840,7 +893,83 @@ describe("assistant oauth connections connect <provider-key>", () => {
840
893
  expect(parsed.error).toBe("Something went wrong");
841
894
  });
842
895
 
896
+ test("succeeds when callbackTransport is null (loopback default)", async () => {
897
+ // Provider row has callbackTransport: null — orchestrator should default
898
+ // to loopback and not require a public ingress URL.
899
+ mockGetMostRecentAppByProvider = () => ({
900
+ id: "app-loopback",
901
+ clientId: "loopback-client",
902
+ clientSecretCredentialPath: "oauth_app/app-loopback/client_secret",
903
+ providerKey: "integration:test-loopback",
904
+ createdAt: 0,
905
+ updatedAt: 0,
906
+ });
907
+
908
+ let capturedOpts: Record<string, unknown> | undefined;
909
+ mockOrchestrateOAuthConnect = async (opts) => {
910
+ capturedOpts = opts;
911
+ return {
912
+ success: true,
913
+ deferred: true,
914
+ authUrl: "https://example.com/auth?loopback",
915
+ state: "state-loopback",
916
+ service: "integration:test-loopback",
917
+ };
918
+ };
919
+
920
+ const { exitCode, stdout } = await runCli([
921
+ "connections",
922
+ "connect",
923
+ "integration:test-loopback",
924
+ "--json",
925
+ ]);
926
+ expect(exitCode).toBe(0);
927
+ const parsed = JSON.parse(stdout);
928
+ expect(parsed.ok).toBe(true);
929
+ expect(parsed.deferred).toBe(true);
930
+ expect(capturedOpts).toBeDefined();
931
+ expect(capturedOpts!.clientId).toBe("loopback-client");
932
+ });
933
+
934
+ test("returns ingress URL error when callbackTransport is explicitly gateway", async () => {
935
+ // Provider row has callbackTransport: "gateway" — orchestrator should
936
+ // require a public ingress URL, which is not configured in the test env.
937
+ mockGetMostRecentAppByProvider = () => ({
938
+ id: "app-gateway",
939
+ clientId: "gateway-client",
940
+ clientSecretCredentialPath: "oauth_app/app-gateway/client_secret",
941
+ providerKey: "integration:test-gateway",
942
+ createdAt: 0,
943
+ updatedAt: 0,
944
+ });
945
+
946
+ mockOrchestrateOAuthConnect = async () => ({
947
+ success: false,
948
+ error:
949
+ "oauth2_connect from a non-interactive session requires a public ingress URL. Configure ingress.publicBaseUrl first.",
950
+ });
951
+
952
+ const { exitCode, stdout } = await runCli([
953
+ "connections",
954
+ "connect",
955
+ "integration:test-gateway",
956
+ "--json",
957
+ ]);
958
+ expect(exitCode).toBe(1);
959
+ const parsed = JSON.parse(stdout);
960
+ expect(parsed.ok).toBe(false);
961
+ expect(parsed.error).toContain("requires a public ingress URL");
962
+ });
963
+
843
964
  test("fails when client_secret is required but missing", async () => {
965
+ mockGetAppByProviderAndClientId = () => ({
966
+ id: "app-1",
967
+ clientId: "test-id",
968
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
969
+ providerKey: "integration:google",
970
+ createdAt: 0,
971
+ updatedAt: 0,
972
+ });
844
973
  mockGetProviderBehavior = () => ({
845
974
  setup: {
846
975
  requiresClientSecret: true,
@@ -853,7 +982,7 @@ describe("assistant oauth connections connect <provider-key>", () => {
853
982
  const { exitCode, stdout } = await runCli([
854
983
  "connections",
855
984
  "connect",
856
- "integration:gmail",
985
+ "integration:google",
857
986
  "--client-id",
858
987
  "test-id",
859
988
  "--json",
@@ -881,7 +1010,7 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
881
1010
  mockUpsertAppCalls = [];
882
1011
  mockUpsertAppResult = {
883
1012
  id: "app-upsert-1",
884
- providerKey: "integration:gmail",
1013
+ providerKey: "integration:google",
885
1014
  clientId: "abc123",
886
1015
  createdAt: 1700000000000,
887
1016
  updatedAt: 1700000000000,
@@ -896,6 +1025,8 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
896
1025
  mockGetProvider = () => undefined;
897
1026
  mockGetProviderBehavior = () => undefined;
898
1027
  mockGetSecureKey = () => undefined;
1028
+ mockGetCredentialMetadata = () => undefined;
1029
+ mockUpsertAppImpl = undefined;
899
1030
  });
900
1031
 
901
1032
  test("upsert with --client-secret-credential-path passes path to upsertApp", async () => {
@@ -903,7 +1034,7 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
903
1034
  "apps",
904
1035
  "upsert",
905
1036
  "--provider",
906
- "integration:gmail",
1037
+ "integration:google",
907
1038
  "--client-id",
908
1039
  "abc123",
909
1040
  "--client-secret-credential-path",
@@ -913,7 +1044,7 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
913
1044
  expect(exitCode).toBe(0);
914
1045
  expect(mockUpsertAppCalls).toHaveLength(1);
915
1046
  expect(mockUpsertAppCalls[0]).toEqual({
916
- provider: "integration:gmail",
1047
+ provider: "integration:google",
917
1048
  clientId: "abc123",
918
1049
  clientSecretOpts: { clientSecretCredentialPath: "custom/path" },
919
1050
  });
@@ -926,7 +1057,7 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
926
1057
  "apps",
927
1058
  "upsert",
928
1059
  "--provider",
929
- "integration:gmail",
1060
+ "integration:google",
930
1061
  "--client-id",
931
1062
  "abc123",
932
1063
  "--client-secret",
@@ -950,7 +1081,7 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
950
1081
  "apps",
951
1082
  "upsert",
952
1083
  "--provider",
953
- "integration:gmail",
1084
+ "integration:google",
954
1085
  "--client-id",
955
1086
  "abc123",
956
1087
  "--client-secret",
@@ -960,7 +1091,7 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
960
1091
  expect(exitCode).toBe(0);
961
1092
  expect(mockUpsertAppCalls).toHaveLength(1);
962
1093
  expect(mockUpsertAppCalls[0]).toEqual({
963
- provider: "integration:gmail",
1094
+ provider: "integration:google",
964
1095
  clientId: "abc123",
965
1096
  clientSecretOpts: { clientSecretValue: "s3cret" },
966
1097
  });
@@ -971,7 +1102,7 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
971
1102
  "apps",
972
1103
  "upsert",
973
1104
  "--provider",
974
- "integration:gmail",
1105
+ "integration:google",
975
1106
  "--client-id",
976
1107
  "abc123",
977
1108
  "--json",
@@ -979,11 +1110,105 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
979
1110
  expect(exitCode).toBe(0);
980
1111
  expect(mockUpsertAppCalls).toHaveLength(1);
981
1112
  expect(mockUpsertAppCalls[0]).toEqual({
982
- provider: "integration:gmail",
1113
+ provider: "integration:google",
983
1114
  clientId: "abc123",
984
1115
  clientSecretOpts: undefined,
985
1116
  });
986
1117
  });
1118
+
1119
+ test("upsert resolves non-prefixed credential path via metadata store", async () => {
1120
+ // The resolution logic splits on the LAST colon, so
1121
+ // "integration:google:client_secret" → service="integration:google", field="client_secret"
1122
+ mockGetCredentialMetadata = (service, field) =>
1123
+ service === "integration:google" && field === "client_secret"
1124
+ ? {
1125
+ credentialId: "cred-1",
1126
+ service: "integration:google",
1127
+ field: "client_secret",
1128
+ allowedTools: [],
1129
+ allowedDomains: [],
1130
+ createdAt: Date.now(),
1131
+ updatedAt: Date.now(),
1132
+ }
1133
+ : undefined;
1134
+
1135
+ const { exitCode, stdout } = await runCli([
1136
+ "apps",
1137
+ "upsert",
1138
+ "--provider",
1139
+ "integration:google",
1140
+ "--client-id",
1141
+ "abc",
1142
+ "--client-secret-credential-path",
1143
+ "integration:google:client_secret",
1144
+ "--json",
1145
+ ]);
1146
+ expect(exitCode).toBe(0);
1147
+ expect(mockUpsertAppCalls).toHaveLength(1);
1148
+ // The non-prefixed path should have been resolved to the full credential key
1149
+ expect(mockUpsertAppCalls[0]).toEqual({
1150
+ provider: "integration:google",
1151
+ clientId: "abc",
1152
+ clientSecretOpts: {
1153
+ clientSecretCredentialPath:
1154
+ "credential/integration:google/client_secret",
1155
+ },
1156
+ });
1157
+ const parsed = JSON.parse(stdout);
1158
+ expect(parsed.id).toBe("app-upsert-1");
1159
+ });
1160
+
1161
+ test("upsert passes prefixed credential path through unchanged", async () => {
1162
+ const { exitCode, stdout } = await runCli([
1163
+ "apps",
1164
+ "upsert",
1165
+ "--provider",
1166
+ "integration:google",
1167
+ "--client-id",
1168
+ "abc",
1169
+ "--client-secret-credential-path",
1170
+ "credential/integration:google/client_secret",
1171
+ "--json",
1172
+ ]);
1173
+ expect(exitCode).toBe(0);
1174
+ expect(mockUpsertAppCalls).toHaveLength(1);
1175
+ // Already-prefixed path should be passed through as-is
1176
+ expect(mockUpsertAppCalls[0]).toEqual({
1177
+ provider: "integration:google",
1178
+ clientId: "abc",
1179
+ clientSecretOpts: {
1180
+ clientSecretCredentialPath:
1181
+ "credential/integration:google/client_secret",
1182
+ },
1183
+ });
1184
+ const parsed = JSON.parse(stdout);
1185
+ expect(parsed.id).toBe("app-upsert-1");
1186
+ });
1187
+
1188
+ test("upsert with invalid credential path returns error when no secret found", async () => {
1189
+ // Override upsertApp to throw when given an unresolvable credential path
1190
+ mockUpsertAppImpl = async (_provider, _clientId, clientSecretOpts) => {
1191
+ throw new Error(
1192
+ `No secret found at credential path: ${clientSecretOpts?.clientSecretCredentialPath}`,
1193
+ );
1194
+ };
1195
+
1196
+ const { exitCode, stdout } = await runCli([
1197
+ "apps",
1198
+ "upsert",
1199
+ "--provider",
1200
+ "integration:google",
1201
+ "--client-id",
1202
+ "abc",
1203
+ "--client-secret-credential-path",
1204
+ "bogus:nonexistent:path",
1205
+ "--json",
1206
+ ]);
1207
+ expect(exitCode).toBe(1);
1208
+ const parsed = JSON.parse(stdout);
1209
+ expect(parsed.ok).toBe(false);
1210
+ expect(parsed.error).toContain("No secret found");
1211
+ });
987
1212
  });
988
1213
 
989
1214
  // ---------------------------------------------------------------------------
@@ -1002,7 +1227,7 @@ describe("assistant oauth connections ping <provider-key>", () => {
1002
1227
 
1003
1228
  test("returns ok when ping endpoint returns 200", async () => {
1004
1229
  mockGetProvider = () => ({
1005
- providerKey: "integration:gmail",
1230
+ providerKey: "integration:google",
1006
1231
  pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1007
1232
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1008
1233
  tokenUrl: "https://oauth2.googleapis.com/token",
@@ -1019,7 +1244,7 @@ describe("assistant oauth connections ping <provider-key>", () => {
1019
1244
  const { exitCode, stdout } = await runCli([
1020
1245
  "connections",
1021
1246
  "ping",
1022
- "integration:gmail",
1247
+ "integration:google",
1023
1248
  "--json",
1024
1249
  ]);
1025
1250
  expect(exitCode).toBe(0);
@@ -1071,7 +1296,7 @@ describe("assistant oauth connections ping <provider-key>", () => {
1071
1296
 
1072
1297
  test("exits 1 when ping endpoint returns non-2xx", async () => {
1073
1298
  mockGetProvider = () => ({
1074
- providerKey: "integration:gmail",
1299
+ providerKey: "integration:google",
1075
1300
  pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1076
1301
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1077
1302
  tokenUrl: "https://oauth2.googleapis.com/token",
@@ -1082,19 +1307,20 @@ describe("assistant oauth connections ping <provider-key>", () => {
1082
1307
  updatedAt: Date.now(),
1083
1308
  });
1084
1309
  const originalFetch = globalThis.fetch;
1310
+ // Use 403 (not 401) — 401 now throws inside withValidToken for retry
1085
1311
  globalThis.fetch = (async () =>
1086
- new Response("Unauthorized", { status: 401 })) as unknown as typeof fetch;
1312
+ new Response("Forbidden", { status: 403 })) as unknown as typeof fetch;
1087
1313
  try {
1088
1314
  const { exitCode, stdout } = await runCli([
1089
1315
  "connections",
1090
1316
  "ping",
1091
- "integration:gmail",
1317
+ "integration:google",
1092
1318
  "--json",
1093
1319
  ]);
1094
1320
  expect(exitCode).toBe(1);
1095
1321
  const parsed = JSON.parse(stdout);
1096
1322
  expect(parsed.ok).toBe(false);
1097
- expect(parsed.status).toBe(401);
1323
+ expect(parsed.status).toBe(403);
1098
1324
  } finally {
1099
1325
  globalThis.fetch = originalFetch;
1100
1326
  }
@@ -1102,10 +1328,10 @@ describe("assistant oauth connections ping <provider-key>", () => {
1102
1328
 
1103
1329
  test("exits 1 when no token exists", async () => {
1104
1330
  mockWithValidToken = async () => {
1105
- throw new Error('No access token found for "integration:gmail".');
1331
+ throw new Error('No access token found for "integration:google".');
1106
1332
  };
1107
1333
  mockGetProvider = () => ({
1108
- providerKey: "integration:gmail",
1334
+ providerKey: "integration:google",
1109
1335
  pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1110
1336
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1111
1337
  tokenUrl: "https://oauth2.googleapis.com/token",
@@ -1118,7 +1344,7 @@ describe("assistant oauth connections ping <provider-key>", () => {
1118
1344
  const { exitCode, stdout } = await runCli([
1119
1345
  "connections",
1120
1346
  "ping",
1121
- "integration:gmail",
1347
+ "integration:google",
1122
1348
  "--json",
1123
1349
  ]);
1124
1350
  expect(exitCode).toBe(1);