@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
@@ -0,0 +1,163 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import {
4
+ CredentialRequiredError,
5
+ PlatformOAuthConnection,
6
+ ProviderUnreachableError,
7
+ } from "./platform-connection.js";
8
+
9
+ const DEFAULT_OPTIONS = {
10
+ id: "conn-1",
11
+ providerKey: "integration:gmail",
12
+ externalId: "ext-123",
13
+ accountInfo: "user@example.com",
14
+ grantedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
15
+ assistantId: "asst-abc",
16
+ platformBaseUrl: "https://platform.example.com",
17
+ apiKey: "test-api-key",
18
+ };
19
+
20
+ describe("PlatformOAuthConnection", () => {
21
+ let originalFetch: typeof globalThis.fetch;
22
+
23
+ beforeEach(() => {
24
+ originalFetch = globalThis.fetch;
25
+ });
26
+
27
+ afterEach(() => {
28
+ globalThis.fetch = originalFetch;
29
+ });
30
+
31
+ test("successful proxied request", async () => {
32
+ const upstreamBody = { messages: [{ id: "msg-1", snippet: "Hello" }] };
33
+
34
+ globalThis.fetch = mock(
35
+ async (url: string | URL | Request, init?: RequestInit) => {
36
+ expect(url).toBe(
37
+ "https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/gmail/",
38
+ );
39
+ expect(init?.method).toBe("POST");
40
+ expect(init?.headers).toEqual({
41
+ Authorization: "Api-Key test-api-key",
42
+ "Content-Type": "application/json",
43
+ });
44
+
45
+ const parsed = JSON.parse(init?.body as string);
46
+ expect(parsed).toEqual({
47
+ request: {
48
+ method: "GET",
49
+ path: "/gmail/v1/users/me/messages",
50
+ query: { maxResults: "10" },
51
+ headers: {},
52
+ body: null,
53
+ },
54
+ });
55
+
56
+ return new Response(
57
+ JSON.stringify({
58
+ status: 200,
59
+ headers: { "content-type": "application/json" },
60
+ body: upstreamBody,
61
+ }),
62
+ { status: 200 },
63
+ );
64
+ },
65
+ ) as unknown as typeof globalThis.fetch;
66
+
67
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
68
+ const result = await conn.request({
69
+ method: "GET",
70
+ path: "/gmail/v1/users/me/messages",
71
+ query: { maxResults: "10" },
72
+ });
73
+
74
+ expect(result.status).toBe(200);
75
+ expect(result.headers).toEqual({ "content-type": "application/json" });
76
+ expect(result.body).toEqual(upstreamBody);
77
+ });
78
+
79
+ test("forwards baseUrl when provided", async () => {
80
+ globalThis.fetch = mock(
81
+ async (_url: string | URL | Request, init?: RequestInit) => {
82
+ const parsed = JSON.parse(init?.body as string);
83
+ expect(parsed.request.baseUrl).toBe(
84
+ "https://www.googleapis.com/calendar/v3",
85
+ );
86
+
87
+ return new Response(
88
+ JSON.stringify({ status: 200, headers: {}, body: {} }),
89
+ { status: 200 },
90
+ );
91
+ },
92
+ ) as unknown as typeof globalThis.fetch;
93
+
94
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
95
+ await conn.request({
96
+ method: "GET",
97
+ path: "/calendars/primary/events",
98
+ baseUrl: "https://www.googleapis.com/calendar/v3",
99
+ });
100
+ });
101
+
102
+ test("omits baseUrl from envelope when not provided", async () => {
103
+ globalThis.fetch = mock(
104
+ async (_url: string | URL | Request, init?: RequestInit) => {
105
+ const parsed = JSON.parse(init?.body as string);
106
+ expect("baseUrl" in parsed.request).toBe(false);
107
+
108
+ return new Response(
109
+ JSON.stringify({ status: 200, headers: {}, body: null }),
110
+ { status: 200 },
111
+ );
112
+ },
113
+ ) as unknown as typeof globalThis.fetch;
114
+
115
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
116
+ await conn.request({ method: "GET", path: "/some/path" });
117
+ });
118
+
119
+ test("424 response throws CredentialRequiredError", async () => {
120
+ globalThis.fetch = mock(async () => {
121
+ return new Response("", { status: 424 });
122
+ }) as unknown as typeof globalThis.fetch;
123
+
124
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
125
+ await expect(
126
+ conn.request({ method: "GET", path: "/test" }),
127
+ ).rejects.toThrow(CredentialRequiredError);
128
+ });
129
+
130
+ test("502 response throws ProviderUnreachableError", async () => {
131
+ globalThis.fetch = mock(async () => {
132
+ return new Response("", { status: 502 });
133
+ }) as unknown as typeof globalThis.fetch;
134
+
135
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
136
+ await expect(
137
+ conn.request({ method: "GET", path: "/test" }),
138
+ ).rejects.toThrow(ProviderUnreachableError);
139
+ });
140
+
141
+ test("withToken throws clear error", async () => {
142
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
143
+ await expect(conn.withToken(async (token) => token)).rejects.toThrow(
144
+ "Raw token access is not supported for platform-managed connections. Use connection.request() instead.",
145
+ );
146
+ });
147
+
148
+ test("strips integration: prefix from providerKey for slug", async () => {
149
+ globalThis.fetch = mock(async (url: string | URL | Request) => {
150
+ expect(String(url)).toContain("/external-provider-proxy/slack/");
151
+ return new Response(
152
+ JSON.stringify({ status: 200, headers: {}, body: null }),
153
+ { status: 200 },
154
+ );
155
+ }) as unknown as typeof globalThis.fetch;
156
+
157
+ const conn = new PlatformOAuthConnection({
158
+ ...DEFAULT_OPTIONS,
159
+ providerKey: "integration:slack",
160
+ });
161
+ await conn.request({ method: "GET", path: "/test" });
162
+ });
163
+ });
@@ -0,0 +1,110 @@
1
+ import type {
2
+ OAuthConnection,
3
+ OAuthConnectionRequest,
4
+ OAuthConnectionResponse,
5
+ } from "./connection.js";
6
+
7
+ export class CredentialRequiredError extends Error {
8
+ constructor(message = "Connection not set up on platform") {
9
+ super(message);
10
+ this.name = "CredentialRequiredError";
11
+ }
12
+ }
13
+
14
+ export class ProviderUnreachableError extends Error {
15
+ constructor(message = "Provider is unreachable") {
16
+ super(message);
17
+ this.name = "ProviderUnreachableError";
18
+ }
19
+ }
20
+
21
+ export interface PlatformOAuthConnectionOptions {
22
+ id: string;
23
+ providerKey: string;
24
+ externalId: string;
25
+ accountInfo: string | null;
26
+ grantedScopes: string[];
27
+ assistantId: string;
28
+ platformBaseUrl: string;
29
+ apiKey: string;
30
+ }
31
+
32
+ export class PlatformOAuthConnection implements OAuthConnection {
33
+ readonly id: string;
34
+ readonly providerKey: string;
35
+ readonly externalId: string;
36
+ readonly accountInfo: string | null;
37
+ readonly grantedScopes: string[];
38
+
39
+ private readonly assistantId: string;
40
+ private readonly platformBaseUrl: string;
41
+ private readonly apiKey: string;
42
+
43
+ constructor(options: PlatformOAuthConnectionOptions) {
44
+ this.id = options.id;
45
+ this.providerKey = options.providerKey;
46
+ this.externalId = options.externalId;
47
+ this.accountInfo = options.accountInfo;
48
+ this.grantedScopes = options.grantedScopes;
49
+ this.assistantId = options.assistantId;
50
+ this.platformBaseUrl = options.platformBaseUrl;
51
+ this.apiKey = options.apiKey;
52
+ }
53
+
54
+ async request(req: OAuthConnectionRequest): Promise<OAuthConnectionResponse> {
55
+ const providerSlug = this.providerKey.replace(/^integration:/, "");
56
+ const proxyUrl = `${this.platformBaseUrl}/v1/assistants/${this.assistantId}/external-provider-proxy/${providerSlug}/`;
57
+
58
+ const body: Record<string, unknown> = {
59
+ request: {
60
+ method: req.method,
61
+ path: req.path,
62
+ query: req.query ?? {},
63
+ headers: req.headers ?? {},
64
+ body: req.body ?? null,
65
+ ...(req.baseUrl ? { baseUrl: req.baseUrl } : {}),
66
+ },
67
+ };
68
+
69
+ const response = await fetch(proxyUrl, {
70
+ method: "POST",
71
+ headers: {
72
+ Authorization: `Api-Key ${this.apiKey}`,
73
+ "Content-Type": "application/json",
74
+ },
75
+ body: JSON.stringify(body),
76
+ });
77
+
78
+ if (response.status === 424) {
79
+ throw new CredentialRequiredError();
80
+ }
81
+
82
+ if (response.status === 502) {
83
+ throw new ProviderUnreachableError();
84
+ }
85
+
86
+ if (!response.ok) {
87
+ throw new Error(
88
+ `Platform proxy returned unexpected status ${response.status}`,
89
+ );
90
+ }
91
+
92
+ const json = (await response.json()) as {
93
+ status: number;
94
+ headers: Record<string, string>;
95
+ body: unknown;
96
+ };
97
+
98
+ return {
99
+ status: json.status,
100
+ headers: json.headers,
101
+ body: json.body,
102
+ };
103
+ }
104
+
105
+ async withToken<T>(_fn: (token: string) => Promise<T>): Promise<T> {
106
+ throw new Error(
107
+ "Raw token access is not supported for platform-managed connections. Use connection.request() instead.",
108
+ );
109
+ }
110
+ }
@@ -0,0 +1,21 @@
1
+ /** Default base URL per credential service. Used by the connection when no per-request override is provided. */
2
+ export const PROVIDER_BASE_URLS: Record<string, string> = {
3
+ "integration:gmail": "https://gmail.googleapis.com/gmail/v1/users/me",
4
+ "integration:slack": "https://slack.com/api",
5
+ "integration:twitter": "https://api.x.com",
6
+ "integration:notion": "https://api.notion.com",
7
+ "integration:linear": "https://api.linear.app",
8
+ "integration:github": "https://api.github.com",
9
+ };
10
+
11
+ /**
12
+ * Alternative base URLs for providers that span multiple API hosts
13
+ * sharing one OAuth token. Callers pass these via `request({ baseUrl })`.
14
+ */
15
+ export const GOOGLE_CALENDAR_BASE_URL =
16
+ "https://www.googleapis.com/calendar/v3";
17
+ export const GOOGLE_PEOPLE_BASE_URL = "https://people.googleapis.com/v1";
18
+
19
+ export function getProviderBaseUrl(providerKey: string): string | undefined {
20
+ return PROVIDER_BASE_URLS[providerKey];
21
+ }
@@ -67,7 +67,7 @@ export const PROVIDER_PROFILES: Record<string, OAuthProviderProfile> = {
67
67
  ],
68
68
  extraParams: { access_type: "offline", prompt: "consent" },
69
69
  callbackTransport: "loopback",
70
- setupSkillId: "google-oauth-setup",
70
+ setupSkillId: "google-oauth-applescript",
71
71
  setup: {
72
72
  displayName: "Google (Gmail & Calendar)",
73
73
  dashboardUrl: "https://console.cloud.google.com/apis/credentials",
@@ -6,6 +6,7 @@
6
6
  * orchestrator without duplicating storage logic.
7
7
  */
8
8
 
9
+ import { credentialKey, migrateKeys } from "../security/credential-key.js";
9
10
  import type {
10
11
  OAuth2FlowResult,
11
12
  TokenEndpointAuthMethod,
@@ -14,10 +15,7 @@ import {
14
15
  deleteSecureKeyAsync,
15
16
  setSecureKeyAsync,
16
17
  } from "../security/secure-keys.js";
17
- import {
18
- deleteCredentialMetadata,
19
- upsertCredentialMetadata,
20
- } from "../tools/credentials/metadata-store.js";
18
+ import { upsertCredentialMetadata } from "../tools/credentials/metadata-store.js";
21
19
  import type { CredentialInjectionTemplate } from "../tools/credentials/policy-types.js";
22
20
  import { runPostConnectHook } from "../tools/credentials/post-connect-hooks.js";
23
21
 
@@ -56,6 +54,8 @@ export interface StoreOAuth2TokensParams {
56
54
  export async function storeOAuth2Tokens(
57
55
  params: StoreOAuth2TokensParams,
58
56
  ): Promise<{ accountInfo?: string }> {
57
+ migrateKeys();
58
+
59
59
  const {
60
60
  service,
61
61
  tokens,
@@ -71,7 +71,7 @@ export async function storeOAuth2Tokens(
71
71
  } = params;
72
72
 
73
73
  const tokenStored = await setSecureKeyAsync(
74
- `credential:${service}:access_token`,
74
+ credentialKey(service, "access_token"),
75
75
  tokens.accessToken,
76
76
  );
77
77
  if (!tokenStored) {
@@ -97,17 +97,11 @@ export async function storeOAuth2Tokens(
97
97
  }
98
98
  }
99
99
 
100
- // Persist client credentials in keychain for defense in depth
101
- const clientIdStored = await setSecureKeyAsync(
102
- `credential:${service}:client_id`,
103
- clientId,
104
- );
105
- if (!clientIdStored) {
106
- throw new Error("Failed to store client_id in secure storage");
107
- }
100
+ // client_id is stored in metadata only (oauth2ClientId field) — not the
101
+ // secure store. token-manager.ts reads it from meta?.oauth2ClientId.
108
102
  if (clientSecret) {
109
103
  const clientSecretStored = await setSecureKeyAsync(
110
- `credential:${service}:client_secret`,
104
+ credentialKey(service, "client_secret"),
111
105
  clientSecret,
112
106
  );
113
107
  if (!clientSecretStored) {
@@ -121,7 +115,6 @@ export async function storeOAuth2Tokens(
121
115
  grantedScopes,
122
116
  oauth2TokenUrl: tokenUrl,
123
117
  oauth2ClientId: clientId,
124
- oauth2ClientSecret: clientSecret ?? null,
125
118
  ...(tokenEndpointAuthMethod
126
119
  ? { oauth2TokenEndpointAuthMethod: tokenEndpointAuthMethod }
127
120
  : {}),
@@ -143,11 +136,14 @@ export async function storeOAuth2Tokens(
143
136
  setNestedValue,
144
137
  } = await import("../config/loader.js");
145
138
  const raw = loadRawConfig();
139
+
140
+ // Write to the namespaced path
146
141
  setNestedValue(
147
142
  raw,
148
- `integrations.accountInfo.${service}`,
143
+ `integrations.${service}.accountInfo`,
149
144
  resolvedAccountInfo,
150
145
  );
146
+
151
147
  saveRawConfig(raw);
152
148
  invalidateConfigCache();
153
149
  } catch {
@@ -157,18 +153,22 @@ export async function storeOAuth2Tokens(
157
153
 
158
154
  if (tokens.refreshToken) {
159
155
  const refreshStored = await setSecureKeyAsync(
160
- `credential:${service}:refresh_token`,
156
+ credentialKey(service, "refresh_token"),
161
157
  tokens.refreshToken,
162
158
  );
163
159
  if (refreshStored) {
164
- upsertCredentialMetadata(service, "refresh_token", {});
160
+ upsertCredentialMetadata(service, "access_token", {
161
+ hasRefreshToken: true,
162
+ });
165
163
  }
166
164
  } else {
167
165
  // Re-auth grants that omit refresh_token must clear any stale stored
168
166
  // token — otherwise withValidToken() will attempt refresh with invalid
169
167
  // credentials.
170
- await deleteSecureKeyAsync(`credential:${service}:refresh_token`);
171
- deleteCredentialMetadata(service, "refresh_token");
168
+ await deleteSecureKeyAsync(credentialKey(service, "refresh_token"));
169
+ upsertCredentialMetadata(service, "access_token", {
170
+ hasRefreshToken: false,
171
+ });
172
172
  }
173
173
 
174
174
  // Run any provider-specific post-connect actions (e.g. Slack welcome DM)
@@ -48,7 +48,11 @@ function riskCacheKey(
48
48
  workingDir?: string,
49
49
  manifestOverride?: ManifestOverride,
50
50
  ): string {
51
- const inputJson = JSON.stringify(input);
51
+ // Strip `reason` before computing the cache key — it is cosmetic and varies
52
+ // per invocation even for identical tool operations, causing unnecessary
53
+ // cache misses.
54
+ const { reason: _reason, ...cacheableInput } = input;
55
+ const inputJson = JSON.stringify(cacheableInput);
52
56
  const hash = createHash("sha256")
53
57
  .update(inputJson)
54
58
  .update("\0")
@@ -139,6 +143,7 @@ const LOW_RISK_PROGRAMS = new Set([
139
143
  "tree",
140
144
  "du",
141
145
  "df",
146
+ "assistant",
142
147
  ]);
143
148
 
144
149
  // High-risk shell programs / patterns
@@ -109,7 +109,11 @@ export function isOnboardingComplete(): boolean {
109
109
  * 3. If BOOTSTRAP.md exists, append first-run ritual instructions
110
110
  * 4. Append skills catalog from ~/.vellum/workspace/skills
111
111
  */
112
- export function buildSystemPrompt(): string {
112
+ export interface BuildSystemPromptOptions {
113
+ hasNoClient?: boolean;
114
+ }
115
+
116
+ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
113
117
  const soulPath = getWorkspacePromptPath("SOUL.md");
114
118
  const identityPath = getWorkspacePromptPath("IDENTITY.md");
115
119
  const userPath = getWorkspacePromptPath("USER.md");
@@ -156,13 +160,14 @@ export function buildSystemPrompt(): string {
156
160
  );
157
161
  }
158
162
  if (getIsContainerized()) parts.push(buildContainerizedSection());
159
- parts.push(buildConfigSection());
163
+ const hasNoClient = options?.hasNoClient ?? false;
164
+ parts.push(buildConfigSection(hasNoClient));
160
165
  parts.push(buildCliReferenceSection());
161
166
  parts.push(buildPostToolResponseSection());
162
167
  parts.push(buildExternalCommsIdentitySection());
163
168
  parts.push(buildChannelAwarenessSection());
164
169
  const config = getConfig();
165
- parts.push(buildToolPermissionSection());
170
+ if (!hasNoClient) parts.push(buildToolPermissionSection());
166
171
  parts.push(buildTaskScheduleReminderRoutingSection());
167
172
  if (
168
173
  isAssistantFeatureFlagEnabled(
@@ -181,9 +186,9 @@ export function buildSystemPrompt(): string {
181
186
  if (!isOnboardingComplete()) {
182
187
  parts.push(buildStarterTaskPlaybookSection());
183
188
  }
184
- parts.push(buildSystemPermissionSection());
189
+ if (!hasNoClient) parts.push(buildSystemPermissionSection());
185
190
  parts.push(buildSwarmGuidanceSection());
186
- parts.push(buildAccessPreferenceSection());
191
+ parts.push(buildAccessPreferenceSection(hasNoClient));
187
192
  parts.push(buildIntegrationSection());
188
193
  parts.push(buildMemoryPersistenceSection());
189
194
  parts.push(buildMemoryRecallSection());
@@ -349,7 +354,7 @@ function buildInChatConfigurationSection(): string {
349
354
  "",
350
355
  "### Avatar Customisation",
351
356
  "",
352
- 'You can change your avatar appearance using the `set_avatar` tool. When the user asks to change, update, or customise your avatar, use `set_avatar` with a `description` parameter describing the desired appearance (e.g. "a friendly purple cat with green eyes wearing a tiny hat"). The tool generates an avatar image and updates all connected clients automatically. If managed avatar generation is configured, no local API key is needed.',
357
+ 'You can change your avatar appearance using the `set_avatar` tool. When the user asks to change, update, or customise your avatar, use `set_avatar` with a `description` parameter describing the desired appearance (e.g. "a friendly purple cat with green eyes wearing a tiny hat"). The tool generates an avatar image and updates all connected clients automatically.',
353
358
  "",
354
359
  "**After generating a new avatar**, always update the `## Avatar` section in `IDENTITY.md` with a brief description of the current avatar appearance. This ensures you remember what you look like across sessions. Example:",
355
360
  "```",
@@ -402,9 +407,9 @@ export function buildPhoneCallsRoutingSection(): string {
402
407
  "### Trigger phrases",
403
408
  '- "Set up phone calling" / "enable calls"',
404
409
  '- "Make a call to..." / "call [number/business]"',
405
- '- "Configure Twilio" (in context of voice calls)',
410
+ '- "Configure Twilio" (for voice calls)',
406
411
  '- "Can you make phone calls?"',
407
- '- "Set up my phone number" (for calling)',
412
+ '- "Set up my phone number"',
408
413
  "",
409
414
  "### What it does",
410
415
  "The skill handles the full phone calling lifecycle:",
@@ -572,7 +577,23 @@ export function buildSwarmGuidanceSection(): string {
572
577
  ].join("\n");
573
578
  }
574
579
 
575
- function buildAccessPreferenceSection(): string {
580
+ function buildAccessPreferenceSection(hasNoClient: boolean): string {
581
+ if (hasNoClient) {
582
+ return [
583
+ "## External Service Access Preference",
584
+ "",
585
+ "When interacting with external services (GitHub, Slack, Linear, Jira, cloud providers, etc.),",
586
+ "follow this priority order:",
587
+ "",
588
+ "1. **Sandbox first (`bash`)** — Always try to do things in your own sandbox environment first.",
589
+ " If a tool (git, curl, jq, etc.) is not installed, install it yourself using `bash`",
590
+ " (e.g. `apt-get install -y git`). The sandbox is your own machine — you have full control.",
591
+ "2. **web_fetch** — For public endpoints or simple API calls that don't need auth.",
592
+ "3. **Browser automation as last resort** — Only when the task genuinely requires a browser",
593
+ " (e.g., no API exists, visual interaction needed, or OAuth consent screen).",
594
+ ].join("\n");
595
+ }
596
+
576
597
  return [
577
598
  "## External Service Access Preference",
578
599
  "",
@@ -635,10 +656,14 @@ function buildIntegrationSection(): string {
635
656
  const raw = loadRawConfig();
636
657
  const lines = ["## Connected Services", ""];
637
658
  for (const cred of oauthCreds) {
638
- const acctInfo = getNestedValue(
659
+ const acctInfo = (getNestedValue(
639
660
  raw,
640
- `integrations.accountInfo.${cred.service}`,
641
- ) as string | undefined;
661
+ `integrations.${cred.service}.accountInfo`,
662
+ ) ??
663
+ // Fallback: legacy config path used before the namespace migration
664
+ getNestedValue(raw, `integrations.accountInfo.${cred.service}`)) as
665
+ | string
666
+ | undefined;
642
667
  const state = acctInfo ? `Connected (${acctInfo})` : "Connected";
643
668
  lines.push(`- **${cred.service}**: ${state}`);
644
669
  }
@@ -751,10 +776,12 @@ function buildPostToolResponseSection(): string {
751
776
  " → Call document_create",
752
777
  "",
753
778
  "For permission-gated tools, send one short context sentence immediately before the tool call so the user can make an informed allow/deny decision.",
779
+ "",
780
+ '**Reason field:** For every tool call, include a `reason` parameter — a brief, non-technical explanation of what you are doing and why. This is shown to the user as a live status update. Use simple language a non-technical person would understand (e.g. "Checking your project settings" not "file_read config.ts").',
754
781
  ].join("\n");
755
782
  }
756
783
 
757
- function buildConfigSection(): string {
784
+ function buildConfigSection(hasNoClient: boolean): string {
758
785
  // Always use `file_edit` (not `host_file_edit`) for workspace files — file_edit
759
786
  // handles sandbox path mapping internally, and host_file_edit is permission-gated
760
787
  // which would trigger approval prompts for routine workspace updates.
@@ -763,10 +790,14 @@ function buildConfigSection(): string {
763
790
  const config = getConfig();
764
791
  const configPreamble = `Your configuration directory is \`${hostWorkspaceDir}/\`.`;
765
792
 
793
+ const fileToolGuidance = hasNoClient
794
+ ? `${configPreamble} **Always use \`file_read\` and \`file_edit\` for these files** — they are inside your sandbox working directory:`
795
+ : `${configPreamble} **Always use \`file_read\` and \`file_edit\` (not \`host_file_read\` / \`host_file_edit\`) for these files** — they are inside your sandbox working directory and do not require host access or user approval:`;
796
+
766
797
  return [
767
798
  "## Configuration",
768
799
  `- **Active model**: \`${config.model}\` (provider: ${config.provider})`,
769
- `${configPreamble} **Always use \`file_read\` and \`file_edit\` (not \`host_file_read\` / \`host_file_edit\`) for these files** — they are inside your sandbox working directory and do not require host access or user approval:`,
800
+ fileToolGuidance,
770
801
  "",
771
802
  "- `IDENTITY.md` — Your name, nature, personality, and emoji. Updated during the first-run ritual.",
772
803
  "- `SOUL.md` — Core principles, personality, and evolution guidance. Your behavioral foundation.",
@@ -801,7 +832,13 @@ function buildConfigSection(): string {
801
832
  "- They rename you or change your role",
802
833
  "- Your avatar appearance changes (update the `## Avatar` section with a description of the new look)",
803
834
  "",
804
- "When reading or updating workspace files, always use the sandbox tools (`file_read`, `file_edit`). Never use `host_file_read` or `host_file_edit` for workspace files — those are for host-only resources outside your workspace.",
835
+ ...(hasNoClient
836
+ ? [
837
+ "When reading or updating workspace files, always use the sandbox tools (`file_read`, `file_edit`).",
838
+ ]
839
+ : [
840
+ "When reading or updating workspace files, always use the sandbox tools (`file_read`, `file_edit`). Never use `host_file_read` or `host_file_edit` for workspace files — those are for host-only resources outside your workspace.",
841
+ ]),
805
842
  "",
806
843
  "When updating, read the file first, then make a targeted edit. Include all useful information, but don't bloat the files over time",
807
844
  ].join("\n");
@@ -40,7 +40,7 @@ Then figure out together:
40
40
  ```
41
41
 
42
42
  The two actions MUST have different labels and prompts. Double-check before calling ui_show that you are not repeating the same suggestion.
43
- If `ui_show` is not available (voice, SMS, or other non-dashboard channels), present the two suggestions as plain text messages instead, numbered so the user can reply with which one they'd like. If the user types a response instead of clicking, continue via the text path. If they want to defer both suggestions and do something else entirely, that's fine too.
43
+ If `ui_show` is not available (voice or other non-dashboard channels), present the two suggestions as plain text messages instead, numbered so the user can reply with which one they'd like. If the user types a response instead of clicking, continue via the text path. If they want to defer both suggestions and do something else entirely, that's fine too.
44
44
 
45
45
  ## Requirements
46
46
 
@@ -16,6 +16,10 @@ export interface GeminiProviderOptions {
16
16
  streamTimeoutMs?: number;
17
17
  /** When set, routes requests through the managed proxy at this base URL. */
18
18
  managedBaseUrl?: string;
19
+ /** Vertex AI project placeholder (used with managed proxy). */
20
+ vertexProject?: string;
21
+ /** Vertex AI location placeholder (used with managed proxy). */
22
+ vertexLocation?: string;
19
23
  }
20
24
 
21
25
  export class GeminiProvider implements Provider {
@@ -29,12 +33,17 @@ export class GeminiProvider implements Provider {
29
33
  model: string,
30
34
  options: GeminiProviderOptions = {},
31
35
  ) {
32
- this.client = new GoogleGenAI({
33
- apiKey,
34
- ...(options.managedBaseUrl
35
- ? { httpOptions: { baseUrl: options.managedBaseUrl } }
36
- : {}),
37
- });
36
+ this.client = options.managedBaseUrl
37
+ ? new GoogleGenAI({
38
+ vertexai: true,
39
+ project: options.vertexProject ?? "proxy",
40
+ location: options.vertexLocation ?? "us-central1",
41
+ httpOptions: {
42
+ baseUrl: options.managedBaseUrl,
43
+ headers: { Authorization: `Bearer ${apiKey}` },
44
+ },
45
+ })
46
+ : new GoogleGenAI({ apiKey });
38
47
  this.model = model;
39
48
  this.streamTimeoutMs = options.streamTimeoutMs ?? 300_000;
40
49
  }
@@ -29,12 +29,12 @@ export const MANAGED_PROVIDER_META: Record<string, ManagedProviderMeta> = {
29
29
  anthropic: {
30
30
  name: "anthropic",
31
31
  managed: true,
32
- proxyPath: "/v1/runtime-proxy/anthropic",
32
+ proxyPath: "/v1/runtime-proxy/vertex",
33
33
  },
34
34
  gemini: {
35
35
  name: "gemini",
36
36
  managed: true,
37
- proxyPath: "/v1/runtime-proxy/gemini",
37
+ proxyPath: "/v1/runtime-proxy/vertex",
38
38
  },
39
39
  fireworks: {
40
40
  name: "fireworks",
@@ -10,11 +10,15 @@
10
10
  */
11
11
 
12
12
  import { getPlatformBaseUrl } from "../../config/env.js";
13
+ import { credentialKey } from "../../security/credential-key.js";
13
14
  import { getSecureKey } from "../../security/secure-keys.js";
14
15
  import { MANAGED_PROVIDER_META } from "./constants.js";
15
16
 
16
17
  /** Storage key for the assistant API key credential. */
17
- const ASSISTANT_API_KEY_STORAGE_KEY = "credential:vellum:assistant_api_key";
18
+ const ASSISTANT_API_KEY_STORAGE_KEY = credentialKey(
19
+ "vellum",
20
+ "assistant_api_key",
21
+ );
18
22
 
19
23
  export interface ManagedProxyContext {
20
24
  /** Whether managed proxy prerequisites are satisfied. */