@vellumai/assistant 0.5.4 → 0.5.6

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 (151) hide show
  1. package/Dockerfile +17 -27
  2. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
  3. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/actor-token-service.test.ts +113 -0
  6. package/src/__tests__/config-schema.test.ts +2 -2
  7. package/src/__tests__/context-window-manager.test.ts +78 -0
  8. package/src/__tests__/conversation-title-service.test.ts +30 -1
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  10. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  11. package/src/__tests__/memory-regressions.test.ts +8 -30
  12. package/src/__tests__/openai-whisper.test.ts +93 -0
  13. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  14. package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
  15. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  16. package/src/__tests__/tool-executor.test.ts +4 -0
  17. package/src/__tests__/volume-security-guard.test.ts +155 -0
  18. package/src/cli/commands/conversations.ts +0 -18
  19. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
  20. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
  21. package/src/config/env-registry.ts +9 -0
  22. package/src/config/env.ts +8 -2
  23. package/src/config/feature-flag-registry.json +8 -8
  24. package/src/config/schema.ts +0 -12
  25. package/src/config/schemas/memory.ts +0 -4
  26. package/src/config/schemas/platform.ts +1 -1
  27. package/src/config/schemas/security.ts +4 -0
  28. package/src/context/window-manager.ts +53 -2
  29. package/src/credential-execution/managed-catalog.ts +5 -15
  30. package/src/daemon/conversation-agent-loop.ts +0 -60
  31. package/src/daemon/conversation-memory.ts +0 -117
  32. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  33. package/src/daemon/daemon-control.ts +7 -0
  34. package/src/daemon/handlers/conversations.ts +0 -11
  35. package/src/daemon/lifecycle.ts +10 -47
  36. package/src/daemon/providers-setup.ts +2 -1
  37. package/src/followups/followup-store.ts +5 -2
  38. package/src/hooks/manager.ts +7 -0
  39. package/src/instrument.ts +33 -1
  40. package/src/memory/conversation-crud.ts +0 -236
  41. package/src/memory/conversation-title-service.ts +26 -10
  42. package/src/memory/db-init.ts +5 -13
  43. package/src/memory/embedding-local.ts +11 -5
  44. package/src/memory/indexer.ts +15 -106
  45. package/src/memory/job-handlers/conversation-starters.ts +24 -36
  46. package/src/memory/job-handlers/embedding.ts +0 -79
  47. package/src/memory/job-utils.ts +1 -1
  48. package/src/memory/jobs-store.ts +0 -8
  49. package/src/memory/jobs-worker.ts +0 -20
  50. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  51. package/src/memory/migrations/index.ts +1 -3
  52. package/src/memory/qdrant-client.ts +4 -6
  53. package/src/memory/schema/conversations.ts +0 -3
  54. package/src/memory/schema/index.ts +0 -2
  55. package/src/messaging/draft-store.ts +2 -2
  56. package/src/messaging/provider.ts +9 -0
  57. package/src/messaging/providers/slack/adapter.ts +29 -2
  58. package/src/oauth/connection-resolver.test.ts +22 -18
  59. package/src/oauth/connection-resolver.ts +92 -7
  60. package/src/oauth/platform-connection.test.ts +78 -69
  61. package/src/oauth/platform-connection.ts +12 -19
  62. package/src/permissions/defaults.ts +3 -3
  63. package/src/permissions/trust-client.ts +332 -0
  64. package/src/permissions/trust-store-interface.ts +105 -0
  65. package/src/permissions/trust-store.ts +531 -39
  66. package/src/platform/client.test.ts +148 -0
  67. package/src/platform/client.ts +71 -0
  68. package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
  69. package/src/providers/speech-to-text/openai-whisper.ts +68 -0
  70. package/src/providers/speech-to-text/resolve.ts +9 -0
  71. package/src/providers/speech-to-text/types.ts +17 -0
  72. package/src/runtime/auth/route-policy.ts +14 -0
  73. package/src/runtime/auth/token-service.ts +133 -0
  74. package/src/runtime/http-server.ts +4 -2
  75. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  76. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  77. package/src/runtime/routes/conversation-routes.ts +2 -1
  78. package/src/runtime/routes/inbound-message-handler.ts +27 -3
  79. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
  80. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
  81. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
  82. package/src/runtime/routes/log-export-routes.ts +1 -0
  83. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  84. package/src/runtime/routes/memory-item-routes.ts +124 -2
  85. package/src/runtime/routes/secret-routes.ts +4 -1
  86. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  87. package/src/schedule/schedule-store.ts +0 -21
  88. package/src/security/ces-credential-client.ts +173 -0
  89. package/src/security/secure-keys.ts +65 -22
  90. package/src/signals/bash.ts +3 -0
  91. package/src/signals/cancel.ts +3 -0
  92. package/src/signals/confirm.ts +3 -0
  93. package/src/signals/conversation-undo.ts +3 -0
  94. package/src/signals/event-stream.ts +7 -0
  95. package/src/signals/shotgun.ts +3 -0
  96. package/src/signals/trust-rule.ts +3 -0
  97. package/src/skills/inline-command-render.ts +5 -1
  98. package/src/skills/inline-command-runner.ts +30 -2
  99. package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
  100. package/src/telemetry/usage-telemetry-reporter.ts +21 -19
  101. package/src/tools/memory/handlers.ts +1 -129
  102. package/src/tools/permission-checker.ts +18 -0
  103. package/src/tools/skills/load.ts +9 -2
  104. package/src/util/device-id.ts +70 -7
  105. package/src/util/logger.ts +35 -9
  106. package/src/util/platform.ts +29 -5
  107. package/src/util/xml.ts +8 -0
  108. package/src/workspace/heartbeat-service.ts +5 -24
  109. package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
  110. package/src/workspace/migrations/registry.ts +2 -0
  111. package/src/__tests__/archive-recall.test.ts +0 -560
  112. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  113. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  114. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  115. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  116. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  117. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  118. package/src/__tests__/memory-brief-time.test.ts +0 -285
  119. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  120. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  121. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  122. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  123. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  124. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  125. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  126. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  127. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  128. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  129. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  130. package/src/__tests__/memory-reducer.test.ts +0 -704
  131. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  132. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  133. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  134. package/src/config/schemas/memory-simplified.ts +0 -101
  135. package/src/memory/archive-recall.ts +0 -516
  136. package/src/memory/archive-store.ts +0 -400
  137. package/src/memory/brief-formatting.ts +0 -33
  138. package/src/memory/brief-open-loops.ts +0 -266
  139. package/src/memory/brief-time.ts +0 -162
  140. package/src/memory/brief.ts +0 -75
  141. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  142. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  143. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  144. package/src/memory/migrations/186-memory-archive.ts +0 -109
  145. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  146. package/src/memory/reducer-scheduler.ts +0 -242
  147. package/src/memory/reducer-store.ts +0 -271
  148. package/src/memory/reducer-types.ts +0 -106
  149. package/src/memory/reducer.ts +0 -467
  150. package/src/memory/schema/memory-archive.ts +0 -121
  151. package/src/memory/schema/memory-brief.ts +0 -55
@@ -1,13 +1,15 @@
1
- import { getPlatformAssistantId } from "../config/env.js";
2
1
  import { getConfig } from "../config/loader.js";
3
2
  import { type Services, ServicesSchema } from "../config/schemas/services.js";
4
- import { resolveManagedProxyContext } from "../providers/managed-proxy/context.js";
3
+ import { VellumPlatformClient } from "../platform/client.js";
5
4
  import { getSecureKeyAsync } from "../security/secure-keys.js";
5
+ import { getLogger } from "../util/logger.js";
6
6
  import { BYOOAuthConnection } from "./byo-connection.js";
7
7
  import type { OAuthConnection } from "./connection.js";
8
8
  import { getActiveConnection, getProvider } from "./oauth-store.js";
9
9
  import { PlatformOAuthConnection } from "./platform-connection.js";
10
10
 
11
+ const log = getLogger("connection-resolver");
12
+
11
13
  export interface ResolveOAuthConnectionOptions {
12
14
  /** OAuth app client ID — narrows to a specific app when multiple BYO apps
13
15
  * exist for the same provider. */
@@ -46,16 +48,32 @@ export async function resolveOAuthConnection(
46
48
  if (managedKey && managedKey in ServicesSchema.shape) {
47
49
  const services: Services = getConfig().services;
48
50
  if (services[managedKey as keyof Services].mode === "managed") {
49
- const ctx = await resolveManagedProxyContext();
50
- const assistantId = getPlatformAssistantId();
51
+ const client = await VellumPlatformClient.create();
52
+ if (!client || !client.platformAssistantId) {
53
+ const detail = !client
54
+ ? "missing platform prerequisites"
55
+ : "missing assistant ID";
56
+ throw new Error(
57
+ `Platform-managed connection for "${providerKey}" cannot be created: ${detail}. ` +
58
+ `Log in to the Vellum platform or switch to using your own OAuth app.`,
59
+ );
60
+ }
61
+
62
+ const providerSlug = providerKey.replace(/^integration:/, "");
63
+
64
+ const connectionId = await resolvePlatformConnectionId({
65
+ client,
66
+ provider: providerSlug,
67
+ account,
68
+ });
69
+
51
70
  return new PlatformOAuthConnection({
52
71
  id: providerKey,
53
72
  providerKey,
54
73
  externalId: providerKey,
55
74
  accountInfo: account ?? null,
56
- assistantId,
57
- platformBaseUrl: ctx.platformBaseUrl,
58
- apiKey: ctx.assistantApiKey,
75
+ client,
76
+ connectionId,
59
77
  });
60
78
  }
61
79
  }
@@ -98,3 +116,70 @@ export async function resolveOAuthConnection(
98
116
  accountInfo: conn.accountInfo,
99
117
  });
100
118
  }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Platform connection ID resolution
122
+ // ---------------------------------------------------------------------------
123
+
124
+ interface ResolvePlatformConnectionIdOptions {
125
+ client: VellumPlatformClient;
126
+ provider: string;
127
+ account?: string;
128
+ }
129
+
130
+ /**
131
+ * Fetch the platform-side connection ID for a managed provider by calling
132
+ * the List Connections endpoint.
133
+ */
134
+ async function resolvePlatformConnectionId(
135
+ options: ResolvePlatformConnectionIdOptions,
136
+ ): Promise<string> {
137
+ const { client, provider, account } = options;
138
+
139
+ const params = new URLSearchParams();
140
+ params.set("provider", provider);
141
+ params.set("status", "ACTIVE");
142
+ if (account) {
143
+ params.set("account_identifier", account);
144
+ }
145
+
146
+ const path = `/v1/assistants/${client.platformAssistantId}/oauth/connections/?${params.toString()}`;
147
+ const response = await client.fetch(path);
148
+
149
+ if (!response.ok) {
150
+ log.error(
151
+ { status: response.status, provider },
152
+ "Failed to list platform OAuth connections",
153
+ );
154
+ throw new Error(
155
+ `Failed to resolve platform connection for "${provider}": HTTP ${response.status}`,
156
+ );
157
+ }
158
+
159
+ const body = (await response.json()) as {
160
+ results?: Array<{ id: string; account_label?: string }>;
161
+ };
162
+ const connections = body.results ?? [];
163
+
164
+ if (connections.length === 0) {
165
+ throw new Error(
166
+ `No active platform OAuth connection found for provider "${provider}"` +
167
+ (account ? ` with account "${account}"` : "") +
168
+ ". Connect the service on the Vellum platform first.",
169
+ );
170
+ }
171
+
172
+ if (connections.length > 1 && !account) {
173
+ log.warn(
174
+ {
175
+ provider,
176
+ count: connections.length,
177
+ selectedId: connections[0].id,
178
+ },
179
+ "Multiple active platform connections found; using the most recently created. " +
180
+ "Pass an account option to select a specific connection.",
181
+ );
182
+ }
183
+
184
+ return connections[0].id;
185
+ }
@@ -1,5 +1,6 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
1
+ import { describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import type { VellumPlatformClient } from "../platform/client.js";
3
4
  import { BackendError, VellumError } from "../util/errors.js";
4
5
  import {
5
6
  CredentialRequiredError,
@@ -7,40 +8,53 @@ import {
7
8
  ProviderUnreachableError,
8
9
  } from "./platform-connection.js";
9
10
 
11
+ function makeMockClient(
12
+ fetchImpl?: typeof globalThis.fetch,
13
+ ): VellumPlatformClient {
14
+ const mockFetchFn =
15
+ fetchImpl ??
16
+ (mock(async () => {
17
+ return new Response(
18
+ JSON.stringify({ status: 200, headers: {}, body: null }),
19
+ { status: 200 },
20
+ );
21
+ }) as unknown as typeof globalThis.fetch);
22
+
23
+ return {
24
+ baseUrl: "https://platform.example.com",
25
+ assistantApiKey: "test-api-key",
26
+ platformAssistantId: "asst-abc",
27
+ fetch: mock(async (path: string, init?: RequestInit) => {
28
+ const url = `https://platform.example.com${path}`;
29
+ const headers = new Headers(init?.headers);
30
+ headers.set("Authorization", "Api-Key test-api-key");
31
+ return mockFetchFn(url, { ...init, headers });
32
+ }),
33
+ } as unknown as VellumPlatformClient;
34
+ }
35
+
10
36
  const DEFAULT_OPTIONS = {
11
37
  id: "conn-1",
12
38
  providerKey: "integration:google",
13
39
  externalId: "ext-123",
14
40
  accountInfo: "user@example.com",
15
- assistantId: "asst-abc",
16
- platformBaseUrl: "https://platform.example.com",
17
- apiKey: "test-api-key",
41
+ client: makeMockClient(),
42
+ connectionId: "platform-conn-123",
18
43
  };
19
44
 
20
45
  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
46
  test("successful proxied request", async () => {
32
47
  const upstreamBody = { messages: [{ id: "msg-1", snippet: "Hello" }] };
33
48
 
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/google/",
49
+ const client = makeMockClient(
50
+ mock(async (url: string | URL | Request, init?: RequestInit) => {
51
+ expect(String(url)).toBe(
52
+ "https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/platform-conn-123/",
38
53
  );
39
54
  expect(init?.method).toBe("POST");
40
- expect(init?.headers).toEqual({
41
- Authorization: "Api-Key test-api-key",
42
- "Content-Type": "application/json",
43
- });
55
+ const headers = new Headers(init?.headers);
56
+ expect(headers.get("Authorization")).toBe("Api-Key test-api-key");
57
+ expect(headers.get("Content-Type")).toBe("application/json");
44
58
 
45
59
  const parsed = JSON.parse(init?.body as string);
46
60
  expect(parsed).toEqual({
@@ -61,10 +75,13 @@ describe("PlatformOAuthConnection", () => {
61
75
  }),
62
76
  { status: 200 },
63
77
  );
64
- },
65
- ) as unknown as typeof globalThis.fetch;
78
+ }) as unknown as typeof globalThis.fetch,
79
+ );
66
80
 
67
- const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
81
+ const conn = new PlatformOAuthConnection({
82
+ ...DEFAULT_OPTIONS,
83
+ client,
84
+ });
68
85
  const result = await conn.request({
69
86
  method: "GET",
70
87
  path: "/gmail/v1/users/me/messages",
@@ -77,8 +94,8 @@ describe("PlatformOAuthConnection", () => {
77
94
  });
78
95
 
79
96
  test("forwards baseUrl when provided", async () => {
80
- globalThis.fetch = mock(
81
- async (_url: string | URL | Request, init?: RequestInit) => {
97
+ const client = makeMockClient(
98
+ mock(async (_url: string | URL | Request, init?: RequestInit) => {
82
99
  const parsed = JSON.parse(init?.body as string);
83
100
  expect(parsed.request.baseUrl).toBe(
84
101
  "https://www.googleapis.com/calendar/v3",
@@ -88,10 +105,10 @@ describe("PlatformOAuthConnection", () => {
88
105
  JSON.stringify({ status: 200, headers: {}, body: {} }),
89
106
  { status: 200 },
90
107
  );
91
- },
92
- ) as unknown as typeof globalThis.fetch;
108
+ }) as unknown as typeof globalThis.fetch,
109
+ );
93
110
 
94
- const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
111
+ const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
95
112
  await conn.request({
96
113
  method: "GET",
97
114
  path: "/calendars/primary/events",
@@ -100,8 +117,8 @@ describe("PlatformOAuthConnection", () => {
100
117
  });
101
118
 
102
119
  test("omits baseUrl from envelope when not provided", async () => {
103
- globalThis.fetch = mock(
104
- async (_url: string | URL | Request, init?: RequestInit) => {
120
+ const client = makeMockClient(
121
+ mock(async (_url: string | URL | Request, init?: RequestInit) => {
105
122
  const parsed = JSON.parse(init?.body as string);
106
123
  expect("baseUrl" in parsed.request).toBe(false);
107
124
 
@@ -109,10 +126,10 @@ describe("PlatformOAuthConnection", () => {
109
126
  JSON.stringify({ status: 200, headers: {}, body: null }),
110
127
  { status: 200 },
111
128
  );
112
- },
113
- ) as unknown as typeof globalThis.fetch;
129
+ }) as unknown as typeof globalThis.fetch,
130
+ );
114
131
 
115
- const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
132
+ const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
116
133
  await conn.request({ method: "GET", path: "/some/path" });
117
134
  });
118
135
 
@@ -127,22 +144,26 @@ describe("PlatformOAuthConnection", () => {
127
144
  });
128
145
 
129
146
  test("424 response throws CredentialRequiredError", async () => {
130
- globalThis.fetch = mock(async () => {
131
- return new Response("", { status: 424 });
132
- }) as unknown as typeof globalThis.fetch;
147
+ const client = makeMockClient(
148
+ mock(
149
+ async () => new Response("", { status: 424 }),
150
+ ) as unknown as typeof globalThis.fetch,
151
+ );
133
152
 
134
- const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
153
+ const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
135
154
  await expect(
136
155
  conn.request({ method: "GET", path: "/test" }),
137
156
  ).rejects.toThrow(CredentialRequiredError);
138
157
  });
139
158
 
140
159
  test("502 response throws ProviderUnreachableError", async () => {
141
- globalThis.fetch = mock(async () => {
142
- return new Response("", { status: 502 });
143
- }) as unknown as typeof globalThis.fetch;
160
+ const client = makeMockClient(
161
+ mock(
162
+ async () => new Response("", { status: 502 }),
163
+ ) as unknown as typeof globalThis.fetch,
164
+ );
144
165
 
145
- const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
166
+ const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
146
167
  await expect(
147
168
  conn.request({ method: "GET", path: "/test" }),
148
169
  ).rejects.toThrow(ProviderUnreachableError);
@@ -155,36 +176,24 @@ describe("PlatformOAuthConnection", () => {
155
176
  );
156
177
  });
157
178
 
158
- test("strips trailing slash from platformBaseUrl to avoid double slashes", async () => {
159
- globalThis.fetch = mock(async (url: string | URL | Request) => {
160
- expect(String(url)).toBe(
161
- "https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/google/",
162
- );
163
- return new Response(
164
- JSON.stringify({ status: 200, headers: {}, body: null }),
165
- { status: 200 },
166
- );
167
- }) as unknown as typeof globalThis.fetch;
168
-
169
- const conn = new PlatformOAuthConnection({
170
- ...DEFAULT_OPTIONS,
171
- platformBaseUrl: "https://platform.example.com/",
172
- });
173
- await conn.request({ method: "GET", path: "/test" });
174
- });
175
-
176
- test("strips integration: prefix from providerKey for slug", async () => {
177
- globalThis.fetch = mock(async (url: string | URL | Request) => {
178
- expect(String(url)).toContain("/external-provider-proxy/slack/");
179
- return new Response(
180
- JSON.stringify({ status: 200, headers: {}, body: null }),
181
- { status: 200 },
182
- );
183
- }) as unknown as typeof globalThis.fetch;
179
+ test("uses connectionId in proxy URL regardless of providerKey format", async () => {
180
+ const client = makeMockClient(
181
+ mock(async (url: string | URL | Request) => {
182
+ expect(String(url)).toContain(
183
+ "/external-provider-proxy/slack-conn-456/",
184
+ );
185
+ return new Response(
186
+ JSON.stringify({ status: 200, headers: {}, body: null }),
187
+ { status: 200 },
188
+ );
189
+ }) as unknown as typeof globalThis.fetch,
190
+ );
184
191
 
185
192
  const conn = new PlatformOAuthConnection({
186
193
  ...DEFAULT_OPTIONS,
194
+ client,
187
195
  providerKey: "integration:slack",
196
+ connectionId: "slack-conn-456",
188
197
  });
189
198
  await conn.request({ method: "GET", path: "/test" });
190
199
  });
@@ -1,3 +1,4 @@
1
+ import type { VellumPlatformClient } from "../platform/client.js";
1
2
  import { BackendError } from "../util/errors.js";
2
3
  import type {
3
4
  OAuthConnection,
@@ -24,9 +25,9 @@ export interface PlatformOAuthConnectionOptions {
24
25
  providerKey: string;
25
26
  externalId: string;
26
27
  accountInfo: string | null;
27
- assistantId: string;
28
- platformBaseUrl: string;
29
- apiKey: string;
28
+ client: VellumPlatformClient;
29
+ /** Platform-side connection ID used in the proxy URL path. */
30
+ connectionId: string;
30
31
  }
31
32
 
32
33
  export class PlatformOAuthConnection implements OAuthConnection {
@@ -35,18 +36,13 @@ export class PlatformOAuthConnection implements OAuthConnection {
35
36
  readonly externalId: string;
36
37
  readonly accountInfo: string | null;
37
38
 
38
- private readonly assistantId: string;
39
- private readonly platformBaseUrl: string;
40
- private readonly apiKey: string;
39
+ private readonly client: VellumPlatformClient;
40
+ private readonly connectionId: string;
41
41
 
42
42
  constructor(options: PlatformOAuthConnectionOptions) {
43
- const missing: string[] = [];
44
- if (!options.platformBaseUrl) missing.push("platform base URL");
45
- if (!options.apiKey) missing.push("assistant API key");
46
- if (!options.assistantId) missing.push("assistant ID");
47
- if (missing.length > 0) {
43
+ if (!options.connectionId) {
48
44
  throw new BackendError(
49
- `Platform-managed connection for "${options.providerKey}" cannot be created: missing ${missing.join(", ")}. ` +
45
+ `Platform-managed connection for "${options.providerKey}" cannot be created: missing connection ID. ` +
50
46
  `Log in to the Vellum platform or switch to using your own OAuth app.`,
51
47
  );
52
48
  }
@@ -55,14 +51,12 @@ export class PlatformOAuthConnection implements OAuthConnection {
55
51
  this.providerKey = options.providerKey;
56
52
  this.externalId = options.externalId;
57
53
  this.accountInfo = options.accountInfo;
58
- this.assistantId = options.assistantId;
59
- this.platformBaseUrl = options.platformBaseUrl.replace(/\/+$/, "");
60
- this.apiKey = options.apiKey;
54
+ this.client = options.client;
55
+ this.connectionId = options.connectionId;
61
56
  }
62
57
 
63
58
  async request(req: OAuthConnectionRequest): Promise<OAuthConnectionResponse> {
64
- const providerSlug = this.providerKey.replace(/^integration:/, "");
65
- const proxyUrl = `${this.platformBaseUrl}/v1/assistants/${this.assistantId}/external-provider-proxy/${providerSlug}/`;
59
+ const proxyPath = `/v1/assistants/${this.client.platformAssistantId}/external-provider-proxy/${this.connectionId}/`;
66
60
 
67
61
  const body: Record<string, unknown> = {
68
62
  request: {
@@ -75,10 +69,9 @@ export class PlatformOAuthConnection implements OAuthConnection {
75
69
  },
76
70
  };
77
71
 
78
- const response = await fetch(proxyUrl, {
72
+ const response = await this.client.fetch(proxyPath, {
79
73
  method: "POST",
80
74
  headers: {
81
- Authorization: `Api-Key ${this.apiKey}`,
82
75
  "Content-Type": "application/json",
83
76
  },
84
77
  body: JSON.stringify(body),
@@ -2,7 +2,7 @@ import { join } from "node:path";
2
2
 
3
3
  import { getConfig } from "../config/loader.js";
4
4
  import { getBundledSkillsDir } from "../config/skills.js";
5
- import { getRootDir } from "../util/platform.js";
5
+ import { getWorkspaceDir } from "../util/platform.js";
6
6
 
7
7
  export interface DefaultRuleTemplate {
8
8
  id: string;
@@ -116,7 +116,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
116
116
  // Workspace prompt files — the agent should always be able to read, edit,
117
117
  // and write these without prompting. Also allow `rm BOOTSTRAP.md` so the
118
118
  // agent can delete it at the end of the onboarding ritual.
119
- const workspaceDir = join(getRootDir(), "workspace").replaceAll("\\", "/");
119
+ const workspaceDir = getWorkspaceDir().replaceAll("\\", "/");
120
120
  const WORKSPACE_PROMPT_FILES = [
121
121
  "IDENTITY.md",
122
122
  "USER.md",
@@ -163,7 +163,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
163
163
  // Skill source directories — writing or editing skill source files should
164
164
  // require explicit user approval so a compromised agent loop cannot silently
165
165
  // modify skill code to escalate privileges.
166
- const managedSkillsDir = join(getRootDir(), "workspace", "skills").replaceAll(
166
+ const managedSkillsDir = join(getWorkspaceDir(), "skills").replaceAll(
167
167
  "\\",
168
168
  "/",
169
169
  );