@vellumai/assistant 0.5.11 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -483,13 +483,9 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
483
483
  });
484
484
 
485
485
  test("upsertCredentialMetadata does not accept oauth2ClientSecret or other OAuth fields", () => {
486
- const record = upsertCredentialMetadata(
487
- "integration:google",
488
- "access_token",
489
- {
490
- allowedTools: ["api_request"],
491
- },
492
- );
486
+ const record = upsertCredentialMetadata("google", "access_token", {
487
+ allowedTools: ["api_request"],
488
+ });
493
489
  expect("oauth2ClientSecret" in record).toBe(false);
494
490
  expect("oauth2TokenUrl" in record).toBe(false);
495
491
  expect("oauth2ClientId" in record).toBe(false);
@@ -497,14 +493,14 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
497
493
 
498
494
  test("client secret is read from secure store, not metadata", async () => {
499
495
  await setSecureKeyAsync(
500
- credentialKey("integration:google", "client_secret"),
496
+ credentialKey("google", "client_secret"),
501
497
  "my-secret",
502
498
  );
503
- upsertCredentialMetadata("integration:google", "access_token", {
499
+ upsertCredentialMetadata("google", "access_token", {
504
500
  allowedTools: ["api_request"],
505
501
  });
506
502
 
507
- const meta = getCredentialMetadata("integration:google", "access_token");
503
+ const meta = getCredentialMetadata("google", "access_token");
508
504
  expect(meta).toBeDefined();
509
505
  expect("oauth2ClientSecret" in meta!).toBe(false);
510
506
  // OAuth-specific fields are no longer in metadata (v5)
@@ -513,9 +509,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
513
509
 
514
510
  // Secret is in secure store
515
511
  expect(
516
- await getSecureKeyAsync(
517
- credentialKey("integration:google", "client_secret"),
518
- ),
512
+ await getSecureKeyAsync(credentialKey("google", "client_secret")),
519
513
  ).toBe("my-secret");
520
514
  });
521
515
 
@@ -525,7 +519,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
525
519
  credentials: [
526
520
  {
527
521
  credentialId: "cred-v2-secret",
528
- service: "integration:google",
522
+ service: "google",
529
523
  field: "access_token",
530
524
  allowedTools: [],
531
525
  allowedDomains: [],
@@ -543,7 +537,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
543
537
  "utf-8",
544
538
  );
545
539
 
546
- const meta = getCredentialMetadata("integration:google", "access_token");
540
+ const meta = getCredentialMetadata("google", "access_token");
547
541
  expect(meta).toBeDefined();
548
542
  expect("oauth2ClientSecret" in meta!).toBe(false);
549
543
 
@@ -755,8 +755,8 @@ describe("credential_store tool — prompt action", () => {
755
755
  describe("credential_store tool — oauth2_connect error paths", () => {
756
756
  /** Well-known provider rows returned by the mocked getProvider */
757
757
  const wellKnownProviders: Record<string, object> = {
758
- "integration:google": {
759
- key: "integration:google",
758
+ google: {
759
+ key: "google",
760
760
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
761
761
  tokenUrl: "https://oauth2.googleapis.com/token",
762
762
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -764,8 +764,8 @@ describe("credential_store tool — oauth2_connect error paths", () => {
764
764
  callbackTransport: "loopback",
765
765
  loopbackPort: 8756,
766
766
  },
767
- "integration:slack": {
768
- key: "integration:slack",
767
+ slack: {
768
+ key: "slack",
769
769
  authUrl: "https://slack.com/oauth/v2/authorize",
770
770
  tokenUrl: "https://slack.com/api/oauth.v2.access",
771
771
  defaultScopes: JSON.stringify(["channels:read"]),
@@ -779,7 +779,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
779
779
  _setStorePath(STORE_PATH);
780
780
  _resetBackend();
781
781
  _setMetadataPath(join(TEST_DIR, "metadata.json"));
782
- // Return well-known provider rows so vault.ts knows gmail/slack are
782
+ // Return well-known provider rows so vault.ts knows google/slack are
783
783
  // registered, and custom providers return undefined.
784
784
  mockGetProvider.mockImplementation(
785
785
  (key: string) => wellKnownProviders[key] ?? undefined,
@@ -880,22 +880,21 @@ describe("credential_store tool — oauth2_connect error paths", () => {
880
880
  expect(result.content).toContain("mock-auth-url.example.com");
881
881
  });
882
882
 
883
- test("resolves gmail alias to integration:google", async () => {
884
- // Even with alias resolution, missing client_id should still fail
883
+ test("rejects missing client_id for google", async () => {
884
+ // Missing client_id should fail
885
885
  const result = await credentialStoreTool.execute(
886
886
  {
887
887
  action: "oauth2_connect",
888
- service: "gmail",
888
+ service: "google",
889
889
  },
890
890
  _ctx,
891
891
  );
892
892
  expect(result.isError).toBe(true);
893
- // Should NOT require auth_url/token_url/scopes — those are well-known for gmail
894
893
  // Should fail on client_id since none is stored
895
894
  expect(result.content).toContain("client_id is required");
896
895
  });
897
896
 
898
- test("resolves slack alias to integration:slack", async () => {
897
+ test("resolves slack to its canonical name", async () => {
899
898
  const result = await credentialStoreTool.execute(
900
899
  {
901
900
  action: "oauth2_connect",
@@ -912,13 +911,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
912
911
  // and store client_secret in the secure store.
913
912
  mockGetMostRecentAppByProvider.mockImplementation(() => ({
914
913
  id: "test-app-id",
915
- providerKey: "integration:google",
914
+ providerKey: "google",
916
915
  clientId: "stored-client-id-123",
917
916
  clientSecretCredentialPath: "oauth_app/test-app-id/client_secret",
918
917
  createdAt: Date.now(),
919
918
  }));
920
919
  mockGetProvider.mockImplementation(() => ({
921
- key: "integration:google",
920
+ key: "google",
922
921
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
923
922
  tokenUrl: "https://oauth2.googleapis.com/token",
924
923
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -934,16 +933,16 @@ describe("credential_store tool — oauth2_connect error paths", () => {
934
933
  const result = await credentialStoreTool.execute(
935
934
  {
936
935
  action: "oauth2_connect",
937
- service: "gmail",
936
+ service: "google",
938
937
  },
939
938
  { ..._ctx, isInteractive: false },
940
939
  );
941
940
 
942
941
  // Should pass client_id and client_secret checks — the flow proceeds
943
- // through the channel path (gmail uses loopback transport so no
942
+ // through the channel path (google uses loopback transport so no
944
943
  // public ingress URL is needed) and returns the authorization URL.
945
944
  expect(result.isError).toBe(false);
946
- expect(result.content).toContain("To connect gmail, open this link");
945
+ expect(result.content).toContain("To connect google, open this link");
947
946
  expect(result.content).not.toContain("client_id is required");
948
947
  expect(result.content).not.toContain("client_secret is required");
949
948
 
@@ -958,13 +957,10 @@ describe("credential_store tool — oauth2_connect error paths", () => {
958
957
  // most-recent-app heuristic) so the secret comes from the correct app.
959
958
  mockGetAppByProviderAndClientId.mockImplementation(
960
959
  (providerKey: string, cId: string) => {
961
- if (
962
- providerKey === "integration:google" &&
963
- cId === "caller-supplied-client-id"
964
- ) {
960
+ if (providerKey === "google" && cId === "caller-supplied-client-id") {
965
961
  return {
966
962
  id: "matched-app-id",
967
- providerKey: "integration:google",
963
+ providerKey: "google",
968
964
  clientId: "caller-supplied-client-id",
969
965
  clientSecretCredentialPath:
970
966
  "oauth_app/matched-app-id/client_secret",
@@ -975,7 +971,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
975
971
  },
976
972
  );
977
973
  mockGetProvider.mockImplementation(() => ({
978
- key: "integration:google",
974
+ key: "google",
979
975
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
980
976
  tokenUrl: "https://oauth2.googleapis.com/token",
981
977
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -991,7 +987,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
991
987
  const result = await credentialStoreTool.execute(
992
988
  {
993
989
  action: "oauth2_connect",
994
- service: "gmail",
990
+ service: "google",
995
991
  client_id: "caller-supplied-client-id",
996
992
  },
997
993
  { ..._ctx, isInteractive: false },
@@ -999,7 +995,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
999
995
 
1000
996
  // Should succeed — client_secret resolved from the matched app
1001
997
  expect(result.isError).toBe(false);
1002
- expect(result.content).toContain("To connect gmail, open this link");
998
+ expect(result.content).toContain("To connect google, open this link");
1003
999
  // getMostRecentAppByProvider should NOT have been called since client_id was known
1004
1000
  expect(mockGetMostRecentAppByProvider).not.toHaveBeenCalled();
1005
1001
 
@@ -1013,13 +1009,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
1013
1009
  // use getMostRecentAppByProvider (the fallback heuristic).
1014
1010
  mockGetMostRecentAppByProvider.mockImplementation(() => ({
1015
1011
  id: "recent-app-id",
1016
- providerKey: "integration:google",
1012
+ providerKey: "google",
1017
1013
  clientId: "recent-client-id",
1018
1014
  clientSecretCredentialPath: "oauth_app/recent-app-id/client_secret",
1019
1015
  createdAt: Date.now(),
1020
1016
  }));
1021
1017
  mockGetProvider.mockImplementation(() => ({
1022
- key: "integration:google",
1018
+ key: "google",
1023
1019
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1024
1020
  tokenUrl: "https://oauth2.googleapis.com/token",
1025
1021
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
@@ -1035,13 +1031,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
1035
1031
  const result = await credentialStoreTool.execute(
1036
1032
  {
1037
1033
  action: "oauth2_connect",
1038
- service: "gmail",
1034
+ service: "google",
1039
1035
  },
1040
1036
  { ..._ctx, isInteractive: false },
1041
1037
  );
1042
1038
 
1043
1039
  expect(result.isError).toBe(false);
1044
- expect(result.content).toContain("To connect gmail, open this link");
1040
+ expect(result.content).toContain("To connect google, open this link");
1045
1041
  // getAppByProviderAndClientId should NOT have been called since client_id was unknown
1046
1042
  expect(mockGetAppByProviderAndClientId).not.toHaveBeenCalled();
1047
1043
 
@@ -1056,23 +1052,24 @@ describe("credential_store tool — oauth2_connect error paths", () => {
1056
1052
  // report the missing secret error.
1057
1053
  mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
1058
1054
  mockGetProvider.mockImplementation(() => ({
1059
- key: "integration:google",
1055
+ key: "google",
1060
1056
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1061
1057
  tokenUrl: "https://oauth2.googleapis.com/token",
1062
1058
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
1059
+ requiresClientSecret: 1,
1063
1060
  }));
1064
1061
 
1065
1062
  const result = await credentialStoreTool.execute(
1066
1063
  {
1067
1064
  action: "oauth2_connect",
1068
- service: "gmail",
1065
+ service: "google",
1069
1066
  client_id: "unknown-client-id",
1070
1067
  },
1071
1068
  _ctx,
1072
1069
  );
1073
1070
 
1074
1071
  expect(result.isError).toBe(true);
1075
- expect(result.content).toContain("client_secret is required for gmail");
1072
+ expect(result.content).toContain("client_secret is required for google");
1076
1073
  // getMostRecentAppByProvider should NOT have been called
1077
1074
  expect(mockGetMostRecentAppByProvider).not.toHaveBeenCalled();
1078
1075
 
@@ -1087,27 +1084,28 @@ describe("credential_store tool — oauth2_connect error paths", () => {
1087
1084
  // guardrail.
1088
1085
  mockGetMostRecentAppByProvider.mockImplementation(() => ({
1089
1086
  id: "test-app-id-no-secret",
1090
- providerKey: "integration:google",
1087
+ providerKey: "google",
1091
1088
  clientId: "stored-client-id-456",
1092
1089
  createdAt: Date.now(),
1093
1090
  }));
1094
1091
  mockGetProvider.mockImplementation(() => ({
1095
- key: "integration:google",
1092
+ key: "google",
1096
1093
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1097
1094
  tokenUrl: "https://oauth2.googleapis.com/token",
1098
1095
  defaultScopes: JSON.stringify(["https://mail.google.com/"]),
1096
+ requiresClientSecret: 1,
1099
1097
  }));
1100
1098
 
1101
1099
  const result = await credentialStoreTool.execute(
1102
1100
  {
1103
1101
  action: "oauth2_connect",
1104
- service: "gmail",
1102
+ service: "google",
1105
1103
  },
1106
1104
  _ctx,
1107
1105
  );
1108
1106
 
1109
1107
  expect(result.isError).toBe(true);
1110
- expect(result.content).toContain("client_secret is required for gmail");
1108
+ expect(result.content).toContain("client_secret is required for google");
1111
1109
 
1112
1110
  // Reset mocks
1113
1111
  mockGetMostRecentAppByProvider.mockImplementation(() => undefined);
@@ -736,7 +736,7 @@ describe("credential_store tool", () => {
736
736
  await credentialStoreTool.execute(
737
737
  {
738
738
  action: "store",
739
- service: "integration:google",
739
+ service: "google",
740
740
  field: "api_key",
741
741
  value: "test-value",
742
742
  },
@@ -744,9 +744,9 @@ describe("credential_store tool", () => {
744
744
  );
745
745
 
746
746
  // Simulate an active OAuth connection for this service
747
- mockConnections.set("integration:google", {
747
+ mockConnections.set("google", {
748
748
  id: "conn-gmail",
749
- providerKey: "integration:google",
749
+ providerKey: "google",
750
750
  oauthAppId: "app-gmail",
751
751
  expiresAt: Date.now() + 3600_000,
752
752
  });
@@ -754,7 +754,7 @@ describe("credential_store tool", () => {
754
754
  const result = await credentialStoreTool.execute(
755
755
  {
756
756
  action: "delete",
757
- service: "integration:google",
757
+ service: "google",
758
758
  field: "api_key",
759
759
  },
760
760
  _ctx,
@@ -764,9 +764,7 @@ describe("credential_store tool", () => {
764
764
  expect(result.content).toContain("Deleted credential");
765
765
  // Verify disconnectOAuthProvider was called with the service name
766
766
  expect(mockDisconnectOAuthProvider).toHaveBeenCalledTimes(1);
767
- expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith(
768
- "integration:google",
769
- );
767
+ expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith("google");
770
768
  });
771
769
  });
772
770
 
@@ -1354,7 +1352,7 @@ describe("withValidToken refresh deduplication", () => {
1354
1352
  }
1355
1353
 
1356
1354
  test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
1357
- await setupService("integration:google");
1355
+ await setupService("google");
1358
1356
 
1359
1357
  let resolveRefresh!: (value: {
1360
1358
  accessToken: string;
@@ -1378,9 +1376,9 @@ describe("withValidToken refresh deduplication", () => {
1378
1376
 
1379
1377
  // Launch 3 concurrent withValidToken calls — all will get a non-expired
1380
1378
  // token first, call the callback, get a 401, and then try to refresh.
1381
- const p1 = withValidToken("integration:google", callback);
1382
- const p2 = withValidToken("integration:google", callback);
1383
- const p3 = withValidToken("integration:google", callback);
1379
+ const p1 = withValidToken("google", callback);
1380
+ const p2 = withValidToken("google", callback);
1381
+ const p3 = withValidToken("google", callback);
1384
1382
 
1385
1383
  // Let the event loop tick so all 3 calls enter the 401 retry path
1386
1384
  await new Promise((r) => setTimeout(r, 10));
@@ -1402,8 +1400,8 @@ describe("withValidToken refresh deduplication", () => {
1402
1400
  });
1403
1401
 
1404
1402
  test("concurrent refreshes for different services proceed independently", async () => {
1405
- await setupService("integration:google");
1406
- await setupService("integration:slack");
1403
+ await setupService("google");
1404
+ await setupService("slack");
1407
1405
 
1408
1406
  let resolveGmail!: (value: {
1409
1407
  accessToken: string;
@@ -1447,8 +1445,8 @@ describe("withValidToken refresh deduplication", () => {
1447
1445
  return `slack-${token}`;
1448
1446
  };
1449
1447
 
1450
- const p1 = withValidToken("integration:google", gmailCallback);
1451
- const p2 = withValidToken("integration:slack", slackCallback);
1448
+ const p1 = withValidToken("google", gmailCallback);
1449
+ const p2 = withValidToken("slack", slackCallback);
1452
1450
 
1453
1451
  await new Promise((r) => setTimeout(r, 10));
1454
1452
 
@@ -1466,7 +1464,7 @@ describe("withValidToken refresh deduplication", () => {
1466
1464
  });
1467
1465
 
1468
1466
  test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
1469
- await setupService("integration:google");
1467
+ await setupService("google");
1470
1468
 
1471
1469
  let refreshCount = 0;
1472
1470
  mockRefreshOAuth2Token.mockImplementation(() => {
@@ -1480,25 +1478,19 @@ describe("withValidToken refresh deduplication", () => {
1480
1478
  const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
1481
1479
 
1482
1480
  // First call triggers a refresh (old token → 401 → refresh → token-1)
1483
- const r1 = await withValidToken(
1484
- "integration:google",
1485
- async (token: string) => {
1486
- if (token !== "token-1") throw err401;
1487
- return token;
1488
- },
1489
- );
1481
+ const r1 = await withValidToken("google", async (token: string) => {
1482
+ if (token !== "token-1") throw err401;
1483
+ return token;
1484
+ });
1490
1485
  expect(r1).toBe("token-1");
1491
1486
  expect(refreshCount).toBe(1);
1492
1487
 
1493
1488
  // Second call also triggers a 401 to verify dedup state was cleaned up
1494
1489
  // and a new refresh is allowed (not deduplicated with the first).
1495
- const r2 = await withValidToken(
1496
- "integration:google",
1497
- async (token: string) => {
1498
- if (token !== "token-2") throw err401;
1499
- return token;
1500
- },
1501
- );
1490
+ const r2 = await withValidToken("google", async (token: string) => {
1491
+ if (token !== "token-2") throw err401;
1492
+ return token;
1493
+ });
1502
1494
  expect(r2).toBe("token-2");
1503
1495
  // Second refresh should have happened (not deduplicated with the first,
1504
1496
  // since the first already completed)
@@ -1506,7 +1498,7 @@ describe("withValidToken refresh deduplication", () => {
1506
1498
  });
1507
1499
 
1508
1500
  test("deduplication propagates refresh errors to all waiting callers", async () => {
1509
- await setupService("integration:google");
1501
+ await setupService("google");
1510
1502
 
1511
1503
  mockRefreshOAuth2Token.mockImplementation(() =>
1512
1504
  Promise.reject(
@@ -1524,8 +1516,8 @@ describe("withValidToken refresh deduplication", () => {
1524
1516
  };
1525
1517
 
1526
1518
  // Launch 2 concurrent calls — both should fail with the same error
1527
- const p1 = withValidToken("integration:google", callback);
1528
- const p2 = withValidToken("integration:google", callback);
1519
+ const p1 = withValidToken("google", callback);
1520
+ const p2 = withValidToken("google", callback);
1529
1521
 
1530
1522
  const results = await Promise.allSettled([p1, p2]);
1531
1523
 
@@ -1059,7 +1059,7 @@ describe("assistant credentials CLI", () => {
1059
1059
  const setResult = await runCli([
1060
1060
  "set",
1061
1061
  "--service",
1062
- "integration:google",
1062
+ "google",
1063
1063
  "--field",
1064
1064
  "client_secret",
1065
1065
  "secret123",
@@ -1068,13 +1068,13 @@ describe("assistant credentials CLI", () => {
1068
1068
  expect(setResult.exitCode).toBe(0);
1069
1069
  const setParsed = JSON.parse(setResult.stdout);
1070
1070
  expect(setParsed.ok).toBe(true);
1071
- expect(setParsed.service).toBe("integration:google");
1071
+ expect(setParsed.service).toBe("google");
1072
1072
  expect(setParsed.field).toBe("client_secret");
1073
1073
 
1074
1074
  const revealResult = await runCli([
1075
1075
  "reveal",
1076
1076
  "--service",
1077
- "integration:google",
1077
+ "google",
1078
1078
  "--field",
1079
1079
  "client_secret",
1080
1080
  "--json",
@@ -110,13 +110,13 @@ describe("daemon credential read requests", () => {
110
110
 
111
111
  test("preserves compound credential service names on metadata reads", async () => {
112
112
  const result = await getSecureKeyResultViaDaemon(
113
- credentialKey("integration:google", "client_secret"),
113
+ credentialKey("google", "client_secret"),
114
114
  );
115
115
 
116
116
  expect(result).toEqual({ value: "secret-value", unreachable: false });
117
117
  expect(getRequestBody()).toEqual({
118
118
  type: "credential",
119
- name: "integration:google:client_secret",
119
+ name: "google:client_secret",
120
120
  reveal: true,
121
121
  });
122
122
  });
@@ -50,6 +50,13 @@ describe("first-greeting", () => {
50
50
  expect(isWakeUpGreeting("Wake Up, My Friend.", 0)).toBe(true);
51
51
  });
52
52
 
53
+ it("returns true for punctuation variations", () => {
54
+ writeFileSync(join(tempDir, "BOOTSTRAP.md"), "bootstrap content");
55
+ expect(isWakeUpGreeting("Wake up, my friend!", 0)).toBe(true);
56
+ expect(isWakeUpGreeting("Wake up, my friend?", 0)).toBe(true);
57
+ expect(isWakeUpGreeting("Wake up, my friend", 0)).toBe(true);
58
+ });
59
+
53
60
  it("returns false when content doesn't match wake-up greeting", () => {
54
61
  writeFileSync(join(tempDir, "BOOTSTRAP.md"), "bootstrap content");
55
62
  expect(isWakeUpGreeting("Hello", 0)).toBe(false);
@@ -215,6 +215,29 @@ describe("HostBashProxy", () => {
215
215
  expect(proxy.hasPendingRequest(requestId)).toBe(false);
216
216
  });
217
217
 
218
+ test("sends host_bash_cancel to client on abort", async () => {
219
+ setup();
220
+
221
+ const controller = new AbortController();
222
+ const resultPromise = proxy.request(
223
+ { command: "echo hello" },
224
+ "session-1",
225
+ controller.signal,
226
+ );
227
+
228
+ const sent = sentMessages[0] as Record<string, unknown>;
229
+ const requestId = sent.requestId as string;
230
+
231
+ controller.abort();
232
+ await resultPromise;
233
+
234
+ // Second message should be the cancel
235
+ expect(sentMessages).toHaveLength(2);
236
+ const cancelMsg = sentMessages[1] as Record<string, unknown>;
237
+ expect(cancelMsg.type).toBe("host_bash_cancel");
238
+ expect(cancelMsg.requestId).toBe(requestId);
239
+ });
240
+
218
241
  test("returns immediately if signal already aborted", async () => {
219
242
  setup();
220
243
 
@@ -272,6 +295,62 @@ describe("HostBashProxy", () => {
272
295
  // The promise should reject since dispose rejects pending
273
296
  expect(resultPromise).rejects.toThrow("Host bash proxy disposed");
274
297
  });
298
+
299
+ test("sends host_bash_cancel for each pending request on dispose", () => {
300
+ setup();
301
+
302
+ const p1 = proxy.request({ command: "echo a" }, "session-1");
303
+ const p2 = proxy.request({ command: "echo b" }, "session-1");
304
+ p1.catch(() => {}); // Expected rejection on dispose
305
+ p2.catch(() => {}); // Expected rejection on dispose
306
+
307
+ const requestIds = (sentMessages as Array<Record<string, unknown>>).map(
308
+ (m) => m.requestId as string,
309
+ );
310
+ expect(requestIds).toHaveLength(2);
311
+
312
+ proxy.dispose();
313
+
314
+ // After the 2 request messages, dispose should have sent 2 cancel messages
315
+ const cancelMessages = sentMessages
316
+ .slice(2)
317
+ .filter(
318
+ (m) => (m as Record<string, unknown>).type === "host_bash_cancel",
319
+ ) as Array<Record<string, unknown>>;
320
+ expect(cancelMessages).toHaveLength(2);
321
+ expect(cancelMessages.map((m) => m.requestId)).toContain(requestIds[0]);
322
+ expect(cancelMessages.map((m) => m.requestId)).toContain(requestIds[1]);
323
+ });
324
+ });
325
+
326
+ describe("late resolve after abort", () => {
327
+ test("resolve is a no-op after abort (entry already deleted)", async () => {
328
+ setup();
329
+
330
+ const controller = new AbortController();
331
+ const resultPromise = proxy.request(
332
+ { command: "echo hello" },
333
+ "session-1",
334
+ controller.signal,
335
+ );
336
+
337
+ const sent = sentMessages[0] as Record<string, unknown>;
338
+ const requestId = sent.requestId as string;
339
+
340
+ controller.abort();
341
+ const result = await resultPromise;
342
+ expect(result.content).toContain("Aborted");
343
+
344
+ // Late resolve should be silently ignored (no throw, no double-resolve)
345
+ proxy.resolve(requestId, {
346
+ stdout: "late",
347
+ stderr: "",
348
+ exitCode: 0,
349
+ timedOut: false,
350
+ });
351
+
352
+ expect(proxy.hasPendingRequest(requestId)).toBe(false);
353
+ });
275
354
  });
276
355
 
277
356
  describe("updateSender", () => {