@vellumai/assistant 0.4.45 → 0.4.48

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 (236) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/architecture/memory.md +1 -1
  3. package/docs/architecture/scheduling.md +2 -3
  4. package/docs/architecture/security.md +5 -5
  5. package/docs/trusted-contact-access.md +5 -6
  6. package/package.json +4 -1
  7. package/src/__tests__/avatar-e2e.test.ts +18 -219
  8. package/src/__tests__/avatar-generator.test.ts +5 -57
  9. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  10. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  11. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  12. package/src/__tests__/cli.test.ts +23 -0
  13. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  14. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  15. package/src/__tests__/credential-broker.test.ts +2 -1
  16. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  17. package/src/__tests__/credential-resolve.test.ts +5 -4
  18. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  19. package/src/__tests__/credential-security-invariants.test.ts +104 -7
  20. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  21. package/src/__tests__/credential-vault.test.ts +284 -12
  22. package/src/__tests__/credentials-cli.test.ts +11 -6
  23. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  24. package/src/__tests__/gemini-image-service.test.ts +75 -45
  25. package/src/__tests__/gemini-provider.test.ts +9 -6
  26. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  27. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  28. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  29. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  30. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  31. package/src/__tests__/integration-status.test.ts +53 -21
  32. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  33. package/src/__tests__/media-generate-image.test.ts +63 -2
  34. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  35. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  36. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  37. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  38. package/src/__tests__/schedule-store.test.ts +1 -1
  39. package/src/__tests__/schema-transforms.test.ts +226 -0
  40. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  41. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  42. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  43. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  44. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  45. package/src/__tests__/skills-uninstall.test.ts +2 -2
  46. package/src/__tests__/skills.test.ts +0 -9
  47. package/src/__tests__/slack-channel-config.test.ts +9 -8
  48. package/src/__tests__/slack-share-routes.test.ts +11 -6
  49. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  50. package/src/__tests__/twilio-config.test.ts +2 -1
  51. package/src/__tests__/twilio-provider.test.ts +4 -2
  52. package/src/__tests__/twilio-routes.test.ts +5 -4
  53. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  54. package/src/approvals/AGENTS.md +1 -1
  55. package/src/calls/call-domain.ts +7 -4
  56. package/src/calls/twilio-config.ts +2 -1
  57. package/src/calls/twilio-provider.ts +2 -1
  58. package/src/calls/twilio-rest.ts +2 -2
  59. package/src/cli/commands/browser-relay.ts +40 -15
  60. package/src/cli/commands/credentials.ts +9 -8
  61. package/src/cli/commands/oauth.ts +1 -1
  62. package/src/cli.ts +3 -2
  63. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  64. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  65. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  66. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  67. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  68. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  69. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  70. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  71. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  72. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  73. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  74. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  75. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  76. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  77. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  78. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  79. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  80. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  81. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  82. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  83. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  84. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  85. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  86. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  87. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  88. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  89. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  90. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  91. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  92. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  94. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  95. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  96. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  97. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  98. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  99. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
  101. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  102. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  103. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  104. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  105. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  106. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  107. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  108. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  109. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  110. package/src/config/loader.ts +6 -0
  111. package/src/daemon/computer-use-session.ts +7 -1
  112. package/src/daemon/guardian-action-generators.ts +4 -5
  113. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  114. package/src/daemon/handlers/config-telegram.ts +33 -20
  115. package/src/daemon/lifecycle.ts +9 -1
  116. package/src/daemon/message-types/integrations.ts +1 -0
  117. package/src/daemon/ride-shotgun-handler.ts +3 -1
  118. package/src/daemon/session-messaging.ts +3 -1
  119. package/src/daemon/session-tool-setup.ts +18 -2
  120. package/src/daemon/session.ts +1 -1
  121. package/src/email/providers/index.ts +2 -1
  122. package/src/instrument.ts +15 -1
  123. package/src/media/app-icon-generator.ts +30 -4
  124. package/src/media/avatar-router.ts +28 -62
  125. package/src/media/gemini-image-service.ts +28 -2
  126. package/src/memory/canonical-guardian-store.ts +1 -1
  127. package/src/memory/guardian-action-store.ts +1 -1
  128. package/src/memory/schema/guardian.ts +1 -1
  129. package/src/messaging/provider.ts +16 -10
  130. package/src/messaging/providers/gmail/adapter.ts +40 -23
  131. package/src/messaging/providers/gmail/client.ts +203 -122
  132. package/src/messaging/providers/gmail/people-client.ts +26 -18
  133. package/src/messaging/providers/slack/adapter.ts +29 -19
  134. package/src/messaging/providers/slack/client.ts +265 -78
  135. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  136. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  137. package/src/messaging/registry.ts +2 -1
  138. package/src/oauth/byo-connection.test.ts +436 -0
  139. package/src/oauth/byo-connection.ts +112 -0
  140. package/src/oauth/connect-orchestrator.ts +27 -0
  141. package/src/oauth/connection-resolver.ts +34 -0
  142. package/src/oauth/connection.ts +38 -0
  143. package/src/oauth/platform-connection.test.ts +163 -0
  144. package/src/oauth/platform-connection.ts +110 -0
  145. package/src/oauth/provider-base-urls.ts +21 -0
  146. package/src/oauth/provider-profiles.ts +1 -1
  147. package/src/oauth/token-persistence.ts +20 -20
  148. package/src/permissions/checker.ts +6 -1
  149. package/src/prompts/system-prompt.ts +52 -15
  150. package/src/prompts/templates/BOOTSTRAP.md +1 -1
  151. package/src/providers/gemini/client.ts +15 -6
  152. package/src/providers/managed-proxy/constants.ts +2 -2
  153. package/src/providers/managed-proxy/context.ts +5 -1
  154. package/src/providers/ratelimit.ts +17 -0
  155. package/src/providers/registry.ts +2 -2
  156. package/src/runtime/AGENTS.md +18 -1
  157. package/src/runtime/auth/route-policy.ts +1 -0
  158. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  159. package/src/runtime/channel-readiness-service.ts +168 -195
  160. package/src/runtime/channel-readiness-types.ts +4 -0
  161. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  162. package/src/runtime/guardian-action-followup-executor.ts +1 -2
  163. package/src/runtime/guardian-action-message-composer.ts +3 -23
  164. package/src/runtime/http-server.ts +9 -4
  165. package/src/runtime/http-types.ts +0 -1
  166. package/src/runtime/middleware/rate-limiter.ts +74 -20
  167. package/src/runtime/middleware/twilio-validation.ts +1 -3
  168. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  169. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  170. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  171. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
  172. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
  173. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  174. package/src/runtime/routes/integrations/twilio.ts +6 -5
  175. package/src/runtime/routes/secret-routes.ts +3 -2
  176. package/src/runtime/routes/settings-routes.ts +75 -17
  177. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  178. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  179. package/src/schedule/integration-status.ts +5 -4
  180. package/src/security/credential-key.ts +170 -0
  181. package/src/security/token-manager.ts +36 -7
  182. package/src/tools/apps/definitions.ts +0 -5
  183. package/src/tools/assets/materialize.ts +0 -5
  184. package/src/tools/assets/search.ts +0 -5
  185. package/src/tools/browser/headless-browser.ts +1 -67
  186. package/src/tools/claude-code/claude-code.ts +0 -5
  187. package/src/tools/computer-use/request-computer-control.ts +0 -5
  188. package/src/tools/credentials/broker.ts +6 -4
  189. package/src/tools/credentials/metadata-store.ts +72 -20
  190. package/src/tools/credentials/resolve.ts +2 -1
  191. package/src/tools/credentials/vault.ts +77 -16
  192. package/src/tools/filesystem/edit.ts +1 -6
  193. package/src/tools/filesystem/read.ts +0 -5
  194. package/src/tools/filesystem/write.ts +1 -6
  195. package/src/tools/host-filesystem/edit.ts +1 -6
  196. package/src/tools/host-filesystem/read.ts +1 -6
  197. package/src/tools/host-filesystem/write.ts +1 -6
  198. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  199. package/src/tools/memory/definitions.ts +0 -5
  200. package/src/tools/network/web-fetch.ts +0 -5
  201. package/src/tools/network/web-search.ts +0 -5
  202. package/src/tools/schema-transforms.ts +99 -0
  203. package/src/tools/skills/load.ts +0 -5
  204. package/src/tools/swarm/delegate.ts +0 -5
  205. package/src/tools/system/avatar-generator.ts +3 -44
  206. package/src/tools/ui-surface/definitions.ts +0 -15
  207. package/src/tools/watch/screen-watch.ts +0 -5
  208. package/src/version.ts +10 -0
  209. package/src/watcher/providers/github.ts +51 -52
  210. package/src/watcher/providers/gmail.ts +88 -80
  211. package/src/watcher/providers/google-calendar.ts +93 -86
  212. package/src/watcher/providers/linear.ts +87 -93
  213. package/src/__tests__/avatar-router.test.ts +0 -149
  214. package/src/__tests__/managed-avatar-client.test.ts +0 -337
  215. package/src/config/bundled-skills/doordash/SKILL.md +0 -170
  216. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
  217. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
  218. package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
  219. package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
  220. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
  221. package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
  222. package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
  223. package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
  224. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
  225. package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
  226. package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
  227. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
  228. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
  229. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
  230. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
  231. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
  232. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
  233. package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
  234. package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
  235. package/src/media/avatar-types.ts +0 -53
  236. package/src/media/managed-avatar-client.ts +0 -225
@@ -17,6 +17,7 @@ let mockGenerateResult = {
17
17
  resolvedModel: "gemini-2.5-flash-image",
18
18
  };
19
19
  let mockGenerateError: Error | null = null;
20
+ let lastGenerateCredentials: unknown = null;
20
21
 
21
22
  mock.module("../config/loader.js", () => ({
22
23
  getConfig: () => ({
@@ -27,7 +28,11 @@ mock.module("../config/loader.js", () => ({
27
28
  }));
28
29
 
29
30
  mock.module("../media/gemini-image-service.js", () => ({
30
- generateImage: async (_apiKey: string, _request: Record<string, unknown>) => {
31
+ generateImage: async (
32
+ credentials: unknown,
33
+ _request: Record<string, unknown>,
34
+ ) => {
35
+ lastGenerateCredentials = credentials;
31
36
  if (mockGenerateError) throw mockGenerateError;
32
37
  return mockGenerateResult;
33
38
  },
@@ -37,6 +42,18 @@ mock.module("../media/gemini-image-service.js", () => ({
37
42
  },
38
43
  }));
39
44
 
45
+ let mockManagedBaseUrl: string | undefined;
46
+ let mockManagedProxyContext = {
47
+ enabled: false,
48
+ platformBaseUrl: "",
49
+ assistantApiKey: "",
50
+ };
51
+
52
+ mock.module("../providers/managed-proxy/context.js", () => ({
53
+ buildManagedBaseUrl: () => mockManagedBaseUrl,
54
+ resolveManagedProxyContext: () => mockManagedProxyContext,
55
+ }));
56
+
40
57
  let mockAttachments: Array<{
41
58
  id: string;
42
59
  assistantId: string;
@@ -138,6 +155,13 @@ beforeEach(() => {
138
155
  };
139
156
  mockGenerateError = null;
140
157
  mockAttachments = [];
158
+ lastGenerateCredentials = null;
159
+ mockManagedBaseUrl = undefined;
160
+ mockManagedProxyContext = {
161
+ enabled: false,
162
+ platformBaseUrl: "",
163
+ assistantApiKey: "",
164
+ };
141
165
  });
142
166
 
143
167
  const fakeContext = {
@@ -153,7 +177,7 @@ describe("image-studio skill script wrapper", () => {
153
177
  expect(getTool("media_generate_image")).toBeUndefined();
154
178
  });
155
179
 
156
- test("returns error when no API key is configured", async () => {
180
+ test("returns error when no API key and no managed proxy", async () => {
157
181
  mockApiKey = undefined;
158
182
 
159
183
  const result = await run({ prompt: "a cat" }, fakeContext);
@@ -162,6 +186,43 @@ describe("image-studio skill script wrapper", () => {
162
186
  expect(result.content).toContain("No Gemini API key");
163
187
  });
164
188
 
189
+ test("falls back to managed proxy when no API key is configured", async () => {
190
+ mockApiKey = undefined;
191
+ mockManagedBaseUrl = "https://platform.example.com/v1/runtime-proxy/vertex";
192
+ mockManagedProxyContext = {
193
+ enabled: true,
194
+ platformBaseUrl: "https://platform.example.com",
195
+ assistantApiKey: "managed-key-123",
196
+ };
197
+
198
+ const result = await run({ prompt: "a hippo" }, fakeContext);
199
+
200
+ expect(result.isError).toBe(false);
201
+ expect(result.content).toContain("Generated 1 image");
202
+ expect(lastGenerateCredentials).toEqual({
203
+ type: "managed-proxy",
204
+ assistantApiKey: "managed-key-123",
205
+ baseUrl: "https://platform.example.com/v1/runtime-proxy/vertex",
206
+ });
207
+ });
208
+
209
+ test("prefers direct API key over managed proxy", async () => {
210
+ mockApiKey = "direct-key";
211
+ mockManagedBaseUrl = "https://platform.example.com/v1/runtime-proxy/vertex";
212
+ mockManagedProxyContext = {
213
+ enabled: true,
214
+ platformBaseUrl: "https://platform.example.com",
215
+ assistantApiKey: "managed-key-123",
216
+ };
217
+
218
+ await run({ prompt: "a cat" }, fakeContext);
219
+
220
+ expect(lastGenerateCredentials).toEqual({
221
+ type: "direct",
222
+ apiKey: "direct-key",
223
+ });
224
+ });
225
+
165
226
  test("returns generated image with contentBlocks", async () => {
166
227
  const result = await run({ prompt: "a sunset" }, fakeContext);
167
228
 
@@ -62,6 +62,7 @@ mock.module("../config/loader.js", () => ({
62
62
 
63
63
  // Credential resolver and secure key mocks — must be set up before
64
64
  // session-manager is imported so the proxy uses our test data.
65
+ import { credentialKey } from "../security/credential-key.js";
65
66
  import type { ResolvedCredential } from "../tools/credentials/resolve.js";
66
67
 
67
68
  let resolveByIdResults = new Map<string, ResolvedCredential | undefined>();
@@ -154,7 +155,7 @@ function makeResolved(
154
155
  credentialId,
155
156
  service,
156
157
  field,
157
- storageKey: `credential:${service}:${field}`,
158
+ storageKey: credentialKey(service, field),
158
159
  injectionTemplates: templates,
159
160
  metadata: {
160
161
  credentialId,
@@ -357,7 +358,10 @@ describe("Story E2E: selfie yesterday -> generated image today", () => {
357
358
  };
358
359
  const resolved = makeResolved("cred-story", [tpl]);
359
360
  resolveByIdResults.set("cred-story", resolved);
360
- secureKeyValues.set("credential:test-service:api-key", "fal_test_secret");
361
+ secureKeyValues.set(
362
+ credentialKey("test-service", "api-key"),
363
+ "fal_test_secret",
364
+ );
361
365
 
362
366
  // Drive the proxy step through the bash tool — the actual integration path.
363
367
  // -x "$HTTP_PROXY" forces curl to use the proxy explicitly (macOS curl
@@ -381,7 +385,7 @@ describe("Story E2E: selfie yesterday -> generated image today", () => {
381
385
 
382
386
  await stopAllSessions();
383
387
  resolveByIdResults.delete("cred-story");
384
- secureKeyValues.delete("credential:test-service:api-key");
388
+ secureKeyValues.delete(credentialKey("test-service", "api-key"));
385
389
  } finally {
386
390
  echo.server.close();
387
391
  }
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { MessagingProvider } from "../messaging/provider.js";
4
4
  import type { SendOptions } from "../messaging/provider-types.js";
5
+ import type { OAuthConnection } from "../oauth/connection.js";
5
6
 
6
7
  const sendMessageMock = mock(async (..._args: unknown[]) => ({
7
8
  id: "msg-1",
@@ -23,19 +24,16 @@ const provider: MessagingProvider = {
23
24
  getHistory: async () => [],
24
25
  search: async () => ({ total: 0, messages: [], hasMore: false }),
25
26
  sendMessage: (
26
- token: string,
27
+ connectionOrToken: OAuthConnection | string,
27
28
  conversationId: string,
28
29
  text: string,
29
30
  options?: SendOptions,
30
- ) => sendMessageMock(token, conversationId, text, options),
31
+ ) => sendMessageMock(connectionOrToken, conversationId, text, options),
31
32
  };
32
33
 
33
34
  mock.module("../config/bundled-skills/messaging/tools/shared.js", () => ({
34
35
  resolveProvider: () => provider,
35
- withProviderToken: async (
36
- _provider: MessagingProvider,
37
- fn: (token: string) => Promise<unknown>,
38
- ) => fn("provider-token"),
36
+ getProviderConnection: () => "provider-token",
39
37
  ok: (content: string) => ({ content, isError: false }),
40
38
  err: (content: string) => ({ content, isError: true }),
41
39
  extractHeader: () => "",
@@ -1,5 +1,7 @@
1
1
  import { describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import { credentialKey } from "../security/credential-key.js";
4
+
3
5
  // ---------------------------------------------------------------------------
4
6
  // Mock the underlying dependencies of managed-proxy/context.js rather than
5
7
  // the context module itself. This avoids global mock bleed: other test files
@@ -19,7 +21,7 @@ const actualSecureKeys = await import("../security/secure-keys.js");
19
21
  mock.module("../security/secure-keys.js", () => ({
20
22
  ...actualSecureKeys,
21
23
  getSecureKey: (key: string) => {
22
- if (key === "credential:vellum:assistant_api_key") {
24
+ if (key === credentialKey("vellum", "assistant_api_key")) {
23
25
  return mockAssistantApiKey || null;
24
26
  }
25
27
  return null;
@@ -1,6 +1,36 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import { MANAGED_PROVIDER_META } from "../providers/managed-proxy/constants.js";
4
+ import { credentialKey } from "../security/credential-key.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Mock @google/genai to capture constructor arguments for Gemini base URL
8
+ // assertions. Must be before importing the registry.
9
+ // ---------------------------------------------------------------------------
10
+ let lastGeminiConstructorOpts: Record<string, unknown> | null = null;
11
+
12
+ mock.module("@google/genai", () => ({
13
+ GoogleGenAI: class MockGoogleGenAI {
14
+ constructor(opts: Record<string, unknown>) {
15
+ lastGeminiConstructorOpts = opts;
16
+ }
17
+ models = {
18
+ generateContentStream: async () => ({
19
+ [Symbol.asyncIterator]: async function* () {
20
+ /* no chunks */
21
+ },
22
+ }),
23
+ };
24
+ },
25
+ ApiError: class FakeApiError extends Error {
26
+ status: number;
27
+ constructor(status: number, message: string) {
28
+ super(message);
29
+ this.status = status;
30
+ this.name = "ApiError";
31
+ }
32
+ },
33
+ }));
4
34
 
5
35
  // ---------------------------------------------------------------------------
6
36
  // Mock the underlying dependencies that the real context module relies on.
@@ -16,7 +46,7 @@ mock.module("../config/env.js", () => ({
16
46
 
17
47
  mock.module("../security/secure-keys.js", () => ({
18
48
  getSecureKey: (key: string) => {
19
- if (key === "credential:vellum:assistant_api_key") {
49
+ if (key === credentialKey("vellum", "assistant_api_key")) {
20
50
  return mockAssistantApiKey;
21
51
  }
22
52
  return null;
@@ -72,6 +102,7 @@ function userKeysFor(...names: string[]): Record<string, string> {
72
102
 
73
103
  beforeEach(() => {
74
104
  disableManagedProxy();
105
+ lastGeminiConstructorOpts = null;
75
106
  });
76
107
 
77
108
  describe("managed proxy integration — credential precedence", () => {
@@ -172,6 +203,25 @@ describe("managed proxy integration — credential precedence", () => {
172
203
  expect(baseURL).toContain("/v1/runtime-proxy/vertex");
173
204
  expect(baseURL).not.toContain("/v1/runtime-proxy/anthropic");
174
205
  });
206
+
207
+ test("managed gemini uses vertex proxy path instead of gemini proxy path", () => {
208
+ enableManagedProxy();
209
+ initializeProviders({
210
+ apiKeys: {},
211
+ provider: "anthropic",
212
+ model: "test-model",
213
+ });
214
+
215
+ // The GoogleGenAI constructor was captured by the mock — verify it
216
+ // received httpOptions.baseUrl pointing at the vertex proxy path.
217
+ expect(lastGeminiConstructorOpts).toBeDefined();
218
+ const httpOptions = lastGeminiConstructorOpts!.httpOptions as
219
+ | { baseUrl?: string }
220
+ | undefined;
221
+ expect(httpOptions).toBeDefined();
222
+ expect(httpOptions!.baseUrl).toContain("/v1/runtime-proxy/vertex");
223
+ expect(httpOptions!.baseUrl).not.toContain("/v1/runtime-proxy/gemini");
224
+ });
175
225
  });
176
226
 
177
227
  describe("neither user keys nor managed context → providers not initialized", () => {
@@ -285,10 +335,24 @@ describe("managed proxy integration — constants integrity", () => {
285
335
  }
286
336
  });
287
337
 
288
- test("managed proxy paths are unique across providers", () => {
289
- const paths = Object.values(MANAGED_PROVIDER_META)
290
- .filter((m) => m.managed && m.proxyPath)
291
- .map((m) => m.proxyPath);
292
- expect(new Set(paths).size).toBe(paths.length);
338
+ test("anthropic and gemini route through vertex proxy path", () => {
339
+ expect(MANAGED_PROVIDER_META.anthropic.proxyPath).toBe(
340
+ "/v1/runtime-proxy/vertex",
341
+ );
342
+ expect(MANAGED_PROVIDER_META.gemini.proxyPath).toBe(
343
+ "/v1/runtime-proxy/vertex",
344
+ );
345
+ });
346
+
347
+ test("other providers use their own proxy paths", () => {
348
+ expect(MANAGED_PROVIDER_META.openai.proxyPath).toBe(
349
+ "/v1/runtime-proxy/openai",
350
+ );
351
+ expect(MANAGED_PROVIDER_META.fireworks.proxyPath).toBe(
352
+ "/v1/runtime-proxy/fireworks",
353
+ );
354
+ expect(MANAGED_PROVIDER_META.openrouter.proxyPath).toBe(
355
+ "/v1/runtime-proxy/openrouter",
356
+ );
293
357
  });
294
358
  });
@@ -903,7 +903,7 @@ describe("routing and mode fields", () => {
903
903
  });
904
904
 
905
905
  test("routing hints round-trip through DB raw query", () => {
906
- const hints = { target: "sms" };
906
+ const hints = { target: "telegram" };
907
907
  const job = createSchedule({
908
908
  name: "Raw round-trip",
909
909
  message: "check raw",
@@ -0,0 +1,226 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { ToolDefinition } from "../providers/types.js";
4
+ import {
5
+ injectReasonField,
6
+ REASON_SKIP_SET,
7
+ schemaDefinesProperty,
8
+ } from "../tools/schema-transforms.js";
9
+
10
+ function makeDef(
11
+ name: string,
12
+ schema: object = { type: "object", properties: {}, required: [] },
13
+ ): ToolDefinition {
14
+ return { name, description: `Tool ${name}`, input_schema: schema };
15
+ }
16
+
17
+ describe("REASON_SKIP_SET", () => {
18
+ test("contains expected tool names", () => {
19
+ expect(REASON_SKIP_SET.has("skill_execute")).toBe(true);
20
+ expect(REASON_SKIP_SET.has("bash")).toBe(true);
21
+ expect(REASON_SKIP_SET.has("host_bash")).toBe(true);
22
+ expect(REASON_SKIP_SET.has("request_system_permission")).toBe(true);
23
+ expect(REASON_SKIP_SET.size).toBe(4);
24
+ });
25
+ });
26
+
27
+ describe("injectReasonField", () => {
28
+ test("injects reason on a tool without it", () => {
29
+ const defs = [makeDef("my_tool")];
30
+ const result = injectReasonField(defs);
31
+ const schema = result[0].input_schema as Record<string, unknown>;
32
+ const props = schema.properties as Record<string, unknown>;
33
+ expect(props.reason).toEqual({ type: "string" });
34
+ });
35
+
36
+ test("adds reason to required array", () => {
37
+ const defs = [
38
+ makeDef("my_tool", {
39
+ type: "object",
40
+ properties: { foo: { type: "string" } },
41
+ required: ["foo"],
42
+ }),
43
+ ];
44
+ const result = injectReasonField(defs);
45
+ const schema = result[0].input_schema as Record<string, unknown>;
46
+ expect(schema.required).toEqual(["foo", "reason"]);
47
+ });
48
+
49
+ test("creates required array if missing", () => {
50
+ const defs = [
51
+ makeDef("my_tool", {
52
+ type: "object",
53
+ properties: { foo: { type: "string" } },
54
+ }),
55
+ ];
56
+ const result = injectReasonField(defs);
57
+ const schema = result[0].input_schema as Record<string, unknown>;
58
+ expect(schema.required).toEqual(["reason"]);
59
+ });
60
+
61
+ test("skips tools in skip set (returns unchanged)", () => {
62
+ const defs = [makeDef("bash"), makeDef("host_bash")];
63
+ const result = injectReasonField(defs);
64
+ // Should be the exact same object references
65
+ expect(Object.is(result[0], defs[0])).toBe(true);
66
+ expect(Object.is(result[1], defs[1])).toBe(true);
67
+ // No reason injected
68
+ const schema0 = result[0].input_schema as Record<string, unknown>;
69
+ const props0 = schema0.properties as Record<string, unknown>;
70
+ expect("reason" in props0).toBe(false);
71
+ });
72
+
73
+ test("skips tools that already have reason in properties", () => {
74
+ const defs = [
75
+ makeDef("my_tool", {
76
+ type: "object",
77
+ properties: { reason: { type: "number" } },
78
+ required: [],
79
+ }),
80
+ ];
81
+ const result = injectReasonField(defs);
82
+ // Should be the exact same object reference (no clone needed)
83
+ expect(Object.is(result[0], defs[0])).toBe(true);
84
+ const schema = result[0].input_schema as Record<string, unknown>;
85
+ const props = schema.properties as Record<string, unknown>;
86
+ // Original reason type preserved
87
+ expect(props.reason).toEqual({ type: "number" });
88
+ });
89
+
90
+ test("does NOT mutate original definition objects", () => {
91
+ const originalProps = { foo: { type: "string" } };
92
+ const originalRequired = ["foo"];
93
+ const originalSchema = {
94
+ type: "object",
95
+ properties: originalProps,
96
+ required: originalRequired,
97
+ };
98
+ const defs = [makeDef("my_tool", originalSchema)];
99
+
100
+ const result = injectReasonField(defs);
101
+
102
+ // Original properties object is untouched
103
+ expect("reason" in originalProps).toBe(false);
104
+ // Original required array is untouched
105
+ expect(originalRequired).toEqual(["foo"]);
106
+ // Original schema properties ref is the same object
107
+ expect(Object.is(originalSchema.properties, originalProps)).toBe(true);
108
+
109
+ // Result has different object refs
110
+ const resultSchema = result[0].input_schema as Record<string, unknown>;
111
+ expect(Object.is(resultSchema, originalSchema)).toBe(false);
112
+ expect(Object.is(resultSchema.properties, originalProps)).toBe(false);
113
+ expect(Object.is(resultSchema.required, originalRequired)).toBe(false);
114
+ });
115
+
116
+ test("passes through non-object schemas unchanged", () => {
117
+ const defs = [makeDef("my_tool", { type: "string" })];
118
+ const result = injectReasonField(defs);
119
+ expect(Object.is(result[0], defs[0])).toBe(true);
120
+ });
121
+
122
+ test("passes through schemas without properties unchanged", () => {
123
+ const defs = [makeDef("my_tool", { type: "object" })];
124
+ const result = injectReasonField(defs);
125
+ expect(Object.is(result[0], defs[0])).toBe(true);
126
+ });
127
+
128
+ test("skips tools with reason defined inside allOf member (composite schema)", () => {
129
+ const defs = [
130
+ makeDef("my_tool", {
131
+ type: "object",
132
+ properties: { foo: { type: "string" } },
133
+ allOf: [
134
+ {
135
+ properties: { reason: { type: "string" } },
136
+ },
137
+ ],
138
+ required: [],
139
+ }),
140
+ ];
141
+ const result = injectReasonField(defs);
142
+ // Should be the exact same object reference (no injection)
143
+ expect(Object.is(result[0], defs[0])).toBe(true);
144
+ const schema = result[0].input_schema as Record<string, unknown>;
145
+ const props = schema.properties as Record<string, unknown>;
146
+ // Top-level properties should NOT have reason injected
147
+ expect("reason" in props).toBe(false);
148
+ });
149
+
150
+ test("handles empty definitions array", () => {
151
+ const result = injectReasonField([]);
152
+ expect(result).toEqual([]);
153
+ });
154
+ });
155
+
156
+ describe("schemaDefinesProperty", () => {
157
+ test("returns true for direct properties match", () => {
158
+ const schema = {
159
+ type: "object",
160
+ properties: { reason: { type: "string" } },
161
+ };
162
+ expect(schemaDefinesProperty(schema, "reason")).toBe(true);
163
+ });
164
+
165
+ test("returns true for property in allOf member", () => {
166
+ const schema = {
167
+ allOf: [{ properties: { reason: { type: "string" } } }],
168
+ };
169
+ expect(schemaDefinesProperty(schema, "reason")).toBe(true);
170
+ });
171
+
172
+ test("returns true for property in oneOf member", () => {
173
+ const schema = {
174
+ oneOf: [
175
+ { properties: { foo: { type: "string" } } },
176
+ { properties: { reason: { type: "string" } } },
177
+ ],
178
+ };
179
+ expect(schemaDefinesProperty(schema, "reason")).toBe(true);
180
+ });
181
+
182
+ test("returns true for property in anyOf member", () => {
183
+ const schema = {
184
+ anyOf: [{ properties: { reason: { type: "string" } } }],
185
+ };
186
+ expect(schemaDefinesProperty(schema, "reason")).toBe(true);
187
+ });
188
+
189
+ test("returns true for nested allOf within oneOf", () => {
190
+ const schema = {
191
+ oneOf: [
192
+ {
193
+ allOf: [{ properties: { reason: { type: "string" } } }],
194
+ },
195
+ ],
196
+ };
197
+ expect(schemaDefinesProperty(schema, "reason")).toBe(true);
198
+ });
199
+
200
+ test("returns false when property not defined", () => {
201
+ const schema = {
202
+ type: "object",
203
+ properties: { foo: { type: "string" } },
204
+ };
205
+ expect(schemaDefinesProperty(schema, "reason")).toBe(false);
206
+ });
207
+
208
+ test("returns false for $ref (fail-closed)", () => {
209
+ const schema = { $ref: "#/definitions/Foo" };
210
+ expect(schemaDefinesProperty(schema, "reason")).toBe(false);
211
+ });
212
+
213
+ test("returns false for null schema", () => {
214
+ expect(schemaDefinesProperty(null, "reason")).toBe(false);
215
+ });
216
+
217
+ test("returns false for undefined schema", () => {
218
+ expect(schemaDefinesProperty(undefined, "reason")).toBe(false);
219
+ });
220
+
221
+ test("returns false for non-object schema", () => {
222
+ expect(schemaDefinesProperty("not-an-object", "reason")).toBe(false);
223
+ expect(schemaDefinesProperty(42, "reason")).toBe(false);
224
+ expect(schemaDefinesProperty(true, "reason")).toBe(false);
225
+ });
226
+ });
@@ -1,6 +1,7 @@
1
1
  import * as http from "node:http";
2
2
  import { afterEach, describe, expect, mock, test } from "bun:test";
3
3
 
4
+ import { credentialKey } from "../security/credential-key.js";
4
5
  import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
5
6
  import type { CredentialInjectionTemplate } from "../tools/credentials/policy-types.js";
6
7
  import type { ResolvedCredential } from "../tools/credentials/resolve.js";
@@ -82,7 +83,7 @@ function makeResolved(
82
83
  credentialId,
83
84
  service,
84
85
  field,
85
- storageKey: `credential:${service}:${field}`,
86
+ storageKey: credentialKey(service, field),
86
87
  injectionTemplates: templates,
87
88
  metadata: {
88
89
  credentialId,
@@ -154,7 +155,7 @@ describe("policyCallback credential injection", () => {
154
155
  resolveByIdResults.set("cred-local", resolved);
155
156
  credentialMetadataList.push(resolved.metadata);
156
157
  secureKeyValues.set(
157
- "credential:test-service:api-key",
158
+ credentialKey("test-service", "api-key"),
158
159
  "fal_secretvalue123",
159
160
  );
160
161
 
@@ -194,7 +195,10 @@ describe("policyCallback credential injection", () => {
194
195
  const resolved = makeResolved("cred-bearer", [tpl]);
195
196
  resolveByIdResults.set("cred-bearer", resolved);
196
197
  credentialMetadataList.push(resolved.metadata);
197
- secureKeyValues.set("credential:test-service:api-key", "tok_abc123");
198
+ secureKeyValues.set(
199
+ credentialKey("test-service", "api-key"),
200
+ "tok_abc123",
201
+ );
198
202
 
199
203
  const session = createSession(
200
204
  CONV_ID,
@@ -311,7 +315,7 @@ describe("MITM rewriteCallback credential injection", () => {
311
315
  const tpl = makeTemplate("*.fal.ai", "authorization", "Key ");
312
316
  const resolved = makeResolved("cred-fal", [tpl]);
313
317
  resolveByIdResults.set("cred-fal", resolved);
314
- secureKeyValues.set("credential:test-service:api-key", "fal_secret");
318
+ secureKeyValues.set(credentialKey("test-service", "api-key"), "fal_secret");
315
319
 
316
320
  const templates = new Map([["cred-fal", [tpl]]]);
317
321
  const headers: Record<string, string> = {
@@ -350,7 +354,7 @@ describe("MITM rewriteCallback credential injection", () => {
350
354
 
351
355
  const tpl = makeTemplate("*.fal.ai", "authorization", "Key ");
352
356
  resolveByIdResults.set("cred-fal", makeResolved("cred-fal", [tpl]));
353
- secureKeyValues.set("credential:test-service:api-key", "fal_secret");
357
+ secureKeyValues.set(credentialKey("test-service", "api-key"), "fal_secret");
354
358
 
355
359
  const templates = new Map([["cred-fal", [tpl]]]);
356
360
  const headers: Record<string, string> = {
@@ -479,8 +483,8 @@ describe("composeWith injection", () => {
479
483
  );
480
484
  resolveByServiceFieldResults.set("twilio:auth_token", composedResolved);
481
485
 
482
- secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
483
- secureKeyValues.set("credential:twilio:auth_token", "secret456");
486
+ secureKeyValues.set(credentialKey("twilio", "account_sid"), "ACtest123");
487
+ secureKeyValues.set(credentialKey("twilio", "auth_token"), "secret456");
484
488
 
485
489
  const session = createSession(
486
490
  CONV_ID,
@@ -531,7 +535,7 @@ describe("composeWith injection", () => {
531
535
  );
532
536
  resolveByIdResults.set("cred-primary", primaryResolved);
533
537
  credentialMetadataList.push(primaryResolved.metadata);
534
- secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
538
+ secureKeyValues.set(credentialKey("twilio", "account_sid"), "ACtest123");
535
539
 
536
540
  // Do NOT register the composed credential in resolveByServiceFieldResults
537
541
 
@@ -578,7 +582,10 @@ describe("composeWith injection", () => {
578
582
  const resolved = makeResolved("cred-b64", [tpl]);
579
583
  resolveByIdResults.set("cred-b64", resolved);
580
584
  credentialMetadataList.push(resolved.metadata);
581
- secureKeyValues.set("credential:test-service:api-key", "plaintext");
585
+ secureKeyValues.set(
586
+ credentialKey("test-service", "api-key"),
587
+ "plaintext",
588
+ );
582
589
 
583
590
  const session = createSession(CONV_ID, ["cred-b64"], undefined, DATA_DIR);
584
591
  const started = await startSession(session.id);
@@ -624,7 +631,7 @@ describe("composeWith injection", () => {
624
631
  );
625
632
  resolveByIdResults.set("cred-primary", primaryResolved);
626
633
  credentialMetadataList.push(primaryResolved.metadata);
627
- secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
634
+ secureKeyValues.set(credentialKey("twilio", "account_sid"), "ACtest123");
628
635
 
629
636
  // Composed credential metadata resolves, but no secret value stored
630
637
  const composedResolved = makeResolved(
@@ -634,7 +641,7 @@ describe("composeWith injection", () => {
634
641
  "auth_token",
635
642
  );
636
643
  resolveByServiceFieldResults.set("twilio:auth_token", composedResolved);
637
- // Do NOT set secureKeyValues for "credential:twilio:auth_token"
644
+ // Do NOT set secureKeyValues for credentialKey("twilio", "auth_token")
638
645
 
639
646
  const session = createSession(
640
647
  CONV_ID,
@@ -700,8 +707,11 @@ describe("composeWith injection", () => {
700
707
  composedResolved,
701
708
  );
702
709
 
703
- secureKeyValues.set("credential:my-service:primary-key", "value1");
704
- secureKeyValues.set("credential:my-service:secondary-key", "value2");
710
+ secureKeyValues.set(credentialKey("my-service", "primary-key"), "value1");
711
+ secureKeyValues.set(
712
+ credentialKey("my-service", "secondary-key"),
713
+ "value2",
714
+ );
705
715
 
706
716
  const session = createSession(
707
717
  CONV_ID,
@@ -64,7 +64,7 @@ function makeResolved(
64
64
  credentialId,
65
65
  service: "test-service",
66
66
  field: "api-key",
67
- storageKey: `credential:test-service:api-key`,
67
+ storageKey: `credential/test-service/api-key`,
68
68
  injectionTemplates: templates,
69
69
  metadata: {
70
70
  credentialId,
@@ -56,7 +56,7 @@ function makeResolved(
56
56
  credentialId,
57
57
  service: "test-service",
58
58
  field: "api-key",
59
- storageKey: `credential:test-service:api-key`,
59
+ storageKey: `credential/test-service/api-key`,
60
60
  injectionTemplates: templates,
61
61
  metadata: {
62
62
  credentialId,