@vellumai/assistant 0.5.11 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -57,8 +57,8 @@ describe("onboarding template contracts", () => {
57
57
  test("contains wrapping-up criteria with required conditions", () => {
58
58
  const lower = bootstrap.toLowerCase();
59
59
  expect(lower).toContain("wrapping up");
60
- expect(lower).toContain("done with onboarding");
61
- expect(lower).toContain("vibe");
60
+ expect(lower).toContain("delete");
61
+ expect(lower).toContain("bootstrap.md");
62
62
  expect(lower).toContain("two suggestions");
63
63
  });
64
64
 
@@ -1,15 +1,8 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { homedir, tmpdir } from "node:os";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { afterEach, describe, expect, mock, test } from "bun:test";
6
-
7
- // Mutable homedir override — when set, resolveInstanceDataDir reads from here.
8
- let homedirOverride: string | undefined;
9
- mock.module("node:os", () => ({
10
- homedir: () => homedirOverride ?? homedir(),
11
- tmpdir,
12
- }));
5
+ import { afterEach, describe, expect, test } from "bun:test";
13
6
 
14
7
  import {
15
8
  ensureDataDir,
@@ -28,7 +21,6 @@ import {
28
21
  getWorkspaceHooksDir,
29
22
  getWorkspacePromptPath,
30
23
  getWorkspaceSkillsDir,
31
- resolveInstanceDataDir,
32
24
  } from "../util/platform.js";
33
25
 
34
26
  const originalBaseDataDir = process.env.BASE_DATA_DIR;
@@ -39,7 +31,6 @@ afterEach(() => {
39
31
  } else {
40
32
  process.env.BASE_DATA_DIR = originalBaseDataDir;
41
33
  }
42
- homedirOverride = undefined;
43
34
  });
44
35
 
45
36
  // Baseline path characterization: documents current pre-migration path layout.
@@ -155,159 +146,3 @@ describe("workspace path primitives", () => {
155
146
  expect(getWorkspaceDir()).toBe("/tmp/custom-base/.vellum/workspace");
156
147
  });
157
148
  });
158
-
159
- describe("resolveInstanceDataDir", () => {
160
- function makeTempHome(): string {
161
- const dir = join(
162
- tmpdir(),
163
- `platform-home-${randomBytes(4).toString("hex")}`,
164
- );
165
- mkdirSync(dir, { recursive: true });
166
- homedirOverride = dir;
167
- return dir;
168
- }
169
-
170
- function writeLockfileToHome(
171
- home: string,
172
- data: Record<string, unknown>,
173
- ): void {
174
- writeFileSync(
175
- join(home, ".vellum.lock.json"),
176
- JSON.stringify(data, null, 2),
177
- );
178
- }
179
-
180
- test("returns undefined when no lockfile exists", () => {
181
- makeTempHome();
182
- expect(resolveInstanceDataDir()).toBeUndefined();
183
- });
184
-
185
- test("returns sole local assistant instanceDir when no activeAssistant", () => {
186
- const home = makeTempHome();
187
- writeLockfileToHome(home, {
188
- assistants: [
189
- {
190
- assistantId: "vellum-calm-stork",
191
- cloud: "local",
192
- resources: {
193
- instanceDir:
194
- "/Users/test/.local/share/vellum/assistants/vellum-calm-stork",
195
- },
196
- },
197
- ],
198
- });
199
- expect(resolveInstanceDataDir()).toBe(
200
- "/Users/test/.local/share/vellum/assistants/vellum-calm-stork",
201
- );
202
- });
203
-
204
- test("returns active assistant instanceDir when activeAssistant matches", () => {
205
- const home = makeTempHome();
206
- writeLockfileToHome(home, {
207
- activeAssistant: "vellum-bold-fox",
208
- assistants: [
209
- {
210
- assistantId: "vellum-calm-stork",
211
- cloud: "local",
212
- resources: {
213
- instanceDir:
214
- "/Users/test/.local/share/vellum/assistants/vellum-calm-stork",
215
- },
216
- },
217
- {
218
- assistantId: "vellum-bold-fox",
219
- cloud: "local",
220
- resources: {
221
- instanceDir:
222
- "/Users/test/.local/share/vellum/assistants/vellum-bold-fox",
223
- },
224
- },
225
- ],
226
- });
227
- expect(resolveInstanceDataDir()).toBe(
228
- "/Users/test/.local/share/vellum/assistants/vellum-bold-fox",
229
- );
230
- });
231
-
232
- test("returns undefined when multiple local assistants and no activeAssistant", () => {
233
- const home = makeTempHome();
234
- writeLockfileToHome(home, {
235
- assistants: [
236
- {
237
- assistantId: "vellum-calm-stork",
238
- cloud: "local",
239
- resources: {
240
- instanceDir:
241
- "/Users/test/.local/share/vellum/assistants/vellum-calm-stork",
242
- },
243
- },
244
- {
245
- assistantId: "vellum-bold-fox",
246
- cloud: "local",
247
- resources: {
248
- instanceDir:
249
- "/Users/test/.local/share/vellum/assistants/vellum-bold-fox",
250
- },
251
- },
252
- ],
253
- });
254
- expect(resolveInstanceDataDir()).toBeUndefined();
255
- });
256
-
257
- test("returns undefined when lockfile has no assistants array", () => {
258
- const home = makeTempHome();
259
- writeLockfileToHome(home, { version: 1 });
260
- expect(resolveInstanceDataDir()).toBeUndefined();
261
- });
262
-
263
- test("returns undefined when lockfile is malformed JSON", () => {
264
- const home = makeTempHome();
265
- writeFileSync(join(home, ".vellum.lock.json"), "{{not json");
266
- expect(resolveInstanceDataDir()).toBeUndefined();
267
- });
268
-
269
- test("treats assistants without cloud field as local", () => {
270
- const home = makeTempHome();
271
- writeLockfileToHome(home, {
272
- assistants: [
273
- {
274
- assistantId: "vellum-quiet-owl",
275
- resources: {
276
- instanceDir:
277
- "/Users/test/.local/share/vellum/assistants/vellum-quiet-owl",
278
- },
279
- },
280
- ],
281
- });
282
- expect(resolveInstanceDataDir()).toBe(
283
- "/Users/test/.local/share/vellum/assistants/vellum-quiet-owl",
284
- );
285
- });
286
-
287
- test("ignores cloud assistants when resolving", () => {
288
- const home = makeTempHome();
289
- writeLockfileToHome(home, {
290
- assistants: [
291
- {
292
- assistantId: "vellum-cloud-eagle",
293
- cloud: "platform",
294
- resources: {
295
- instanceDir: "/some/cloud/path",
296
- },
297
- },
298
- {
299
- assistantId: "vellum-local-robin",
300
- cloud: "local",
301
- resources: {
302
- instanceDir:
303
- "/Users/test/.local/share/vellum/assistants/vellum-local-robin",
304
- },
305
- },
306
- ],
307
- });
308
- // Only one local assistant, so it auto-selects
309
- expect(resolveInstanceDataDir()).toBe(
310
- "/Users/test/.local/share/vellum/assistants/vellum-local-robin",
311
- );
312
- });
313
- });
@@ -6,6 +6,7 @@ let lastGeminiConstructorOpts: Record<string, unknown> | null = null;
6
6
  let secureKeyStore: Record<string, string | undefined> = {};
7
7
  const metadataUpserts: Array<{ service: string; field: string }> = [];
8
8
  const metadataDeletes: Array<{ service: string; field: string }> = [];
9
+ let providerRefreshCalls = 0;
9
10
 
10
11
  const PLATFORM_BASE_URL = "https://platform.example.com";
11
12
  const ASSISTANT_API_KEY_PATH = credentialKey("vellum", "assistant_api_key");
@@ -137,6 +138,29 @@ function makeDeleteCredentialRequest(name: string): Request {
137
138
  });
138
139
  }
139
140
 
141
+ function makeAddApiKeyRequest(name: string, value: string): Request {
142
+ return new Request("http://localhost/v1/secrets", {
143
+ method: "POST",
144
+ headers: { "Content-Type": "application/json" },
145
+ body: JSON.stringify({
146
+ type: "api_key",
147
+ name,
148
+ value,
149
+ }),
150
+ });
151
+ }
152
+
153
+ function makeDeleteApiKeyRequest(name: string): Request {
154
+ return new Request("http://localhost/v1/secrets", {
155
+ method: "DELETE",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({
158
+ type: "api_key",
159
+ name,
160
+ }),
161
+ });
162
+ }
163
+
140
164
  describe("secret routes managed proxy registry sync", () => {
141
165
  beforeEach(async () => {
142
166
  secureKeyStore = {};
@@ -144,6 +168,7 @@ describe("secret routes managed proxy registry sync", () => {
144
168
  metadataDeletes.length = 0;
145
169
  lastGeminiConstructorOpts = null;
146
170
  platformBaseUrlOverride = undefined;
171
+ providerRefreshCalls = 0;
147
172
  await initializeProviders(mockConfig);
148
173
  });
149
174
 
@@ -169,6 +194,33 @@ describe("secret routes managed proxy registry sync", () => {
169
194
  expect(lastGeminiConstructorOpts).toBeDefined();
170
195
  });
171
196
 
197
+ test("provider API key writes notify live-conversation refresh listeners", async () => {
198
+ const res = await handleAddSecret(
199
+ makeAddApiKeyRequest("fireworks", "fw-key"),
200
+ {
201
+ onProviderCredentialsChanged: () => {
202
+ providerRefreshCalls++;
203
+ },
204
+ },
205
+ );
206
+
207
+ expect(res.status).toBe(201);
208
+ expect(secureKeyStore.fireworks).toBe("fw-key");
209
+ expect(providerRefreshCalls).toBe(1);
210
+
211
+ const deleteRes = await handleDeleteSecret(
212
+ makeDeleteApiKeyRequest("fireworks"),
213
+ {
214
+ onProviderCredentialsChanged: () => {
215
+ providerRefreshCalls++;
216
+ },
217
+ },
218
+ );
219
+
220
+ expect(deleteRes.status).toBe(200);
221
+ expect(providerRefreshCalls).toBe(2);
222
+ });
223
+
172
224
  test("deleting vellum:assistant_api_key clears managed fallback providers immediately", async () => {
173
225
  secureKeyStore[ASSISTANT_API_KEY_PATH] = "ast-managed-key";
174
226
  await initializeProviders(mockConfig);
@@ -190,6 +242,32 @@ describe("secret routes managed proxy registry sync", () => {
190
242
  expect(listProviders()).toEqual([]);
191
243
  });
192
244
 
245
+ test("managed proxy credential writes notify live-conversation refresh listeners", async () => {
246
+ const res = await handleAddSecret(
247
+ makeAddCredentialRequest("vellum:assistant_api_key", "ast-managed-key"),
248
+ {
249
+ onProviderCredentialsChanged: () => {
250
+ providerRefreshCalls++;
251
+ },
252
+ },
253
+ );
254
+
255
+ expect(res.status).toBe(201);
256
+ expect(providerRefreshCalls).toBe(1);
257
+
258
+ const deleteRes = await handleDeleteSecret(
259
+ makeDeleteCredentialRequest("vellum:assistant_api_key"),
260
+ {
261
+ onProviderCredentialsChanged: () => {
262
+ providerRefreshCalls++;
263
+ },
264
+ },
265
+ );
266
+
267
+ expect(deleteRes.status).toBe(200);
268
+ expect(providerRefreshCalls).toBe(2);
269
+ });
270
+
193
271
  test("storing vellum:platform_base_url sets override and triggers initializeProviders", async () => {
194
272
  const res = await handleAddSecret(
195
273
  makeAddCredentialRequest(
@@ -0,0 +1,73 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../util/logger.js", () => ({
4
+ getLogger: () =>
5
+ new Proxy({} as Record<string, unknown>, {
6
+ get: () => () => {},
7
+ }),
8
+ }));
9
+
10
+ import type { CesClient } from "../credential-execution/client.js";
11
+ import {
12
+ _resetBackend,
13
+ getActiveBackendName,
14
+ getSecureKeyAsync,
15
+ setCesClient,
16
+ } from "../security/secure-keys.js";
17
+
18
+ const rpcCall = mock(async () => ({ found: false }));
19
+ const originalFetch = globalThis.fetch;
20
+
21
+ let rpcReady = true;
22
+
23
+ function createMockCesClient(): CesClient {
24
+ return {
25
+ handshake: mock(async () => ({ accepted: true })),
26
+ call: rpcCall as CesClient["call"],
27
+ updateAssistantApiKey: mock(async () => ({ updated: true })),
28
+ isReady: () => rpcReady,
29
+ close: mock(() => {}),
30
+ };
31
+ }
32
+
33
+ describe("secure-keys managed CES failover", () => {
34
+ beforeEach(() => {
35
+ _resetBackend();
36
+ rpcCall.mockClear();
37
+ rpcReady = true;
38
+ process.env.IS_CONTAINERIZED = "1";
39
+ process.env.CES_CREDENTIAL_URL = "http://localhost:8090";
40
+ process.env.CES_SERVICE_TOKEN = "test-token";
41
+ const mockFetch = mock(async () => {
42
+ return new Response(JSON.stringify({ value: "http-secret" }), {
43
+ status: 200,
44
+ headers: { "Content-Type": "application/json" },
45
+ });
46
+ }) as unknown as typeof fetch;
47
+ mockFetch.preconnect = originalFetch.preconnect;
48
+ globalThis.fetch = mockFetch;
49
+ });
50
+
51
+ afterEach(() => {
52
+ globalThis.fetch = originalFetch;
53
+ delete process.env.IS_CONTAINERIZED;
54
+ delete process.env.CES_CREDENTIAL_URL;
55
+ delete process.env.CES_SERVICE_TOKEN;
56
+ _resetBackend();
57
+ });
58
+
59
+ test("falls back from dead CES RPC transport to CES HTTP in managed mode", async () => {
60
+ setCesClient(createMockCesClient());
61
+
62
+ expect(await getSecureKeyAsync("openai")).toBeUndefined();
63
+ expect(getActiveBackendName()).toBe("ces-rpc");
64
+ expect(rpcCall).toHaveBeenCalledTimes(1);
65
+
66
+ rpcReady = false;
67
+
68
+ expect(await getSecureKeyAsync("openai")).toBe("http-secret");
69
+ expect(getActiveBackendName()).toBe("ces-http");
70
+ expect(rpcCall).toHaveBeenCalledTimes(1);
71
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1);
72
+ });
73
+ });
@@ -19,6 +19,7 @@ afterEach(() => {
19
19
  const DECLARED_FLAG_ID = "contacts";
20
20
  const DECLARED_FLAG_KEY = DECLARED_FLAG_ID;
21
21
  const DECLARED_SKILL_ID = "contacts";
22
+ const APP_BUILDER_MULTIFILE_FLAG_KEY = "app-builder-multifile";
22
23
  // ---------------------------------------------------------------------------
23
24
  // Helpers
24
25
  // ---------------------------------------------------------------------------
@@ -158,6 +159,13 @@ describe("isAssistantFeatureFlagEnabled", () => {
158
159
  // browser is declared in the registry with defaultEnabled: true
159
160
  expect(isAssistantFeatureFlagEnabled("browser", config)).toBe(true);
160
161
  });
162
+
163
+ test("app-builder-multifile defaults to enabled when no override is set", () => {
164
+ const config = makeConfig();
165
+ expect(
166
+ isAssistantFeatureFlagEnabled(APP_BUILDER_MULTIFILE_FLAG_KEY, config),
167
+ ).toBe(true);
168
+ });
161
169
  });
162
170
 
163
171
  // ---------------------------------------------------------------------------
@@ -0,0 +1,212 @@
1
+ import { execSync } from "node:child_process";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ /**
5
+ * Guard test: SKILL.md files must never instruct the assistant to accept
6
+ * secrets (passwords, API keys, tokens, etc.) pasted directly in chat.
7
+ *
8
+ * Secrets must always be collected via `credential_store prompt`, which
9
+ * presents a secure native UI that keeps the value out of conversation
10
+ * history and LLM context.
11
+ *
12
+ * This guard prevents regressions like the gmail/messaging bundled skill
13
+ * violation where SKILL.md contained "Include client_secret too if they
14
+ * provide one" — directing the assistant to accept a secret value from
15
+ * the chat stream.
16
+ */
17
+
18
+ /** SKILL.md files permitted to contain otherwise-violating patterns. */
19
+ const ALLOWLIST = new Set<string>([
20
+ // Add paths here only if there is a genuine, documented exception.
21
+ ]);
22
+
23
+ /**
24
+ * Words that indicate the line is about a secret/credential value.
25
+ */
26
+ const SECRET_WORDS =
27
+ "secret|password|api[_\\s-]?key|auth[_\\s-]?token|private[_\\s-]?key|access[_\\s-]?token|client[_\\s-]?secret|signing[_\\s-]?key|bearer[_\\s-]?token";
28
+
29
+ /**
30
+ * Patterns that indicate the assistant is being told to accept a secret
31
+ * value directly in chat, rather than via credential_store prompt.
32
+ */
33
+ const VIOLATION_PATTERNS: RegExp[] = [
34
+ // "accept <secret> in/via/from chat/plaintext/the conversation"
35
+ new RegExp(
36
+ `accept\\s+.*(?:${SECRET_WORDS}).*\\b(?:in|via|from)\\s+(?:chat|plaintext|the\\s+conversation)`,
37
+ "i",
38
+ ),
39
+ new RegExp(
40
+ `accept\\s+.*\\b(?:in|via|from)\\s+(?:chat|plaintext|the\\s+conversation).*(?:${SECRET_WORDS})`,
41
+ "i",
42
+ ),
43
+ // "ask (the user|them) (for|to share/send/paste/type/provide) <secret>" where destination is chat
44
+ // Must have "the user" or "them" as the object to avoid matching third-party descriptions
45
+ // like "Discord will ask for a 2FA code before revealing the secret"
46
+ new RegExp(
47
+ `ask\\s+(?:the\\s+user|them)\\s+(?:for|to\\s+(?:share|send|paste|type|provide))\\s+(?:the\\s+|their\\s+|a\\s+)?(?:${SECRET_WORDS})`,
48
+ "i",
49
+ ),
50
+ // "Include <secret> too if they provide one" — the original gmail violation pattern
51
+ new RegExp(
52
+ `include\\s+(?:the\\s+)?(?:${SECRET_WORDS})\\s+(?:too|as\\s+well|also)\\s+if\\s+they\\s+provide`,
53
+ "i",
54
+ ),
55
+ // "<secret> pasted/typed/sent in chat/conversation/plaintext"
56
+ new RegExp(
57
+ `(?:${SECRET_WORDS})\\s+(?:pasted|typed|sent|provided|shared)\\s+(?:in|via|through)\\s+(?:chat|conversation|plaintext)`,
58
+ "i",
59
+ ),
60
+ // "paste/type/send <secret> in chat/here/the conversation"
61
+ new RegExp(
62
+ `(?:paste|type|send|share|provide)\\s+(?:the\\s+|your\\s+|their\\s+)?(?:${SECRET_WORDS})\\s+(?:in\\s+(?:chat|the\\s+conversation)|here)`,
63
+ "i",
64
+ ),
65
+ ];
66
+
67
+ /**
68
+ * Lines containing these negation words are typically instructing the
69
+ * assistant NOT to do something — these are not violations.
70
+ */
71
+ const NEGATION_PATTERNS =
72
+ /\b(?:never|do\s+not|don['']t|must\s+not|should\s+not|shouldn['']t)\b|\bNOT\b/;
73
+
74
+ /**
75
+ * Lines that are YAML-style field values within a credential_store prompt
76
+ * block (label, description, placeholder). These contain secret-related
77
+ * words but are secure UI text, not chat instructions.
78
+ */
79
+ const CREDENTIAL_STORE_UI_FIELD =
80
+ /^\s*(?:[-*]\s+)?(?:label|description|placeholder)\s*[:=]\s*/i;
81
+
82
+ interface Violation {
83
+ file: string;
84
+ line: number;
85
+ text: string;
86
+ }
87
+
88
+ function findViolations(): Violation[] {
89
+ const repoRoot = process.cwd() + "/..";
90
+
91
+ // Find all SKILL.md files tracked by git
92
+ let skillFiles: string[];
93
+ try {
94
+ const output = execSync(`git grep -l "" -- '*/SKILL.md'`, {
95
+ encoding: "utf-8",
96
+ cwd: repoRoot,
97
+ }).trim();
98
+ skillFiles = output.split("\n").filter((f) => f.length > 0);
99
+ } catch (err) {
100
+ if ((err as { status?: number }).status === 1) {
101
+ return []; // no SKILL.md files found
102
+ }
103
+ throw err;
104
+ }
105
+
106
+ // Filter to skills/ and assistant/src/config/bundled-skills/ directories
107
+ skillFiles = skillFiles.filter(
108
+ (f) =>
109
+ f.startsWith("skills/") ||
110
+ f.startsWith("assistant/src/config/bundled-skills/"),
111
+ );
112
+
113
+ const violations: Violation[] = [];
114
+
115
+ for (const filePath of skillFiles) {
116
+ if (ALLOWLIST.has(filePath)) continue;
117
+
118
+ let content: string;
119
+ try {
120
+ content = execSync(`git show HEAD:${filePath}`, {
121
+ encoding: "utf-8",
122
+ cwd: repoRoot,
123
+ });
124
+ } catch {
125
+ continue;
126
+ }
127
+
128
+ const lines = content.split("\n");
129
+
130
+ // Track whether we're inside a credential_store prompt block
131
+ // (indented YAML-like content after a credential_store mention)
132
+ let inCredentialStoreBlock = false;
133
+ let blockIndent = 0;
134
+
135
+ for (let i = 0; i < lines.length; i++) {
136
+ const line = lines[i];
137
+ const lineNumber = i + 1;
138
+
139
+ // Track credential_store prompt blocks
140
+ if (/credential_store\s+prompt/i.test(line)) {
141
+ inCredentialStoreBlock = true;
142
+ blockIndent = line.search(/\S/);
143
+ continue;
144
+ }
145
+
146
+ // Exit credential_store block when indentation returns to same or lesser level
147
+ if (inCredentialStoreBlock) {
148
+ const currentIndent = line.search(/\S/);
149
+ if (
150
+ currentIndent !== -1 &&
151
+ currentIndent <= blockIndent &&
152
+ line.trim().length > 0
153
+ ) {
154
+ inCredentialStoreBlock = false;
155
+ }
156
+ }
157
+
158
+ // Skip empty lines
159
+ if (line.trim().length === 0) continue;
160
+
161
+ // Skip negation lines — these instruct NOT to do something
162
+ if (NEGATION_PATTERNS.test(line)) continue;
163
+
164
+ // Skip credential_store UI field lines (label:, description:, placeholder:)
165
+ if (inCredentialStoreBlock && CREDENTIAL_STORE_UI_FIELD.test(line))
166
+ continue;
167
+
168
+ // Strip markdown backticks before pattern matching so that
169
+ // violations like `client_secret` are caught the same as bare words.
170
+ const stripped = line.replace(/`/g, "");
171
+
172
+ // Check against violation patterns
173
+ for (const pattern of VIOLATION_PATTERNS) {
174
+ if (pattern.test(stripped)) {
175
+ violations.push({
176
+ file: filePath,
177
+ line: lineNumber,
178
+ text: line.trim(),
179
+ });
180
+ break; // one violation per line is enough
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ return violations;
187
+ }
188
+
189
+ describe("SKILL.md secret handling guard", () => {
190
+ test("no SKILL.md files instruct accepting secrets in chat", () => {
191
+ const violations = findViolations();
192
+
193
+ if (violations.length > 0) {
194
+ const message = [
195
+ "Found SKILL.md files that instruct accepting secrets directly in chat.",
196
+ "Secrets must always be collected via `credential_store prompt`, which",
197
+ "presents a secure native UI that keeps values out of conversation history.",
198
+ "",
199
+ "Violations:",
200
+ ...violations.map((v) => ` - ${v.file}:${v.line}: ${v.text}`),
201
+ "",
202
+ "To fix: replace chat-based secret collection with a `credential_store prompt` call.",
203
+ "See any *-setup skill (e.g. skills/slack-app-setup/SKILL.md) for the correct pattern.",
204
+ "",
205
+ "If this is a genuine exception, add the file path to the ALLOWLIST in",
206
+ "skill-secret-handling-guard.test.ts.",
207
+ ].join("\n");
208
+
209
+ expect(violations, message).toEqual([]);
210
+ }
211
+ });
212
+ });
@@ -58,7 +58,7 @@ describe("assistant skills uninstall", () => {
58
58
 
59
59
  // GIVEN a skill is installed locally
60
60
  installFakeSkill("weather");
61
- writeSkillsIndex("- weather\n- google-oauth-applescript\n");
61
+ writeSkillsIndex("- weather\n- google-oauth-app-setup\n");
62
62
 
63
63
  // WHEN we uninstall the skill
64
64
  uninstallSkillLocally("weather");
@@ -71,7 +71,7 @@ describe("assistant skills uninstall", () => {
71
71
  expect(index).not.toContain("weather");
72
72
 
73
73
  // AND other skills should remain in the index
74
- expect(index).toContain("google-oauth-applescript");
74
+ expect(index).toContain("google-oauth-app-setup");
75
75
  });
76
76
 
77
77
  test("errors when skill is not installed", () => {