@vellumai/assistant 0.4.37 → 0.4.41

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 (169) hide show
  1. package/ARCHITECTURE.md +3 -3
  2. package/README.md +13 -13
  3. package/bun.lock +80 -24
  4. package/docs/architecture/integrations.md +126 -128
  5. package/docs/runbook-trusted-contacts.md +1 -1
  6. package/docs/trusted-contact-access.md +12 -12
  7. package/package.json +3 -1
  8. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
  9. package/src/__tests__/app-bundler.test.ts +209 -0
  10. package/src/__tests__/app-compiler.test.ts +279 -0
  11. package/src/__tests__/app-executors.test.ts +293 -483
  12. package/src/__tests__/app-migration.test.ts +148 -0
  13. package/src/__tests__/app-routes-csp.test.ts +202 -0
  14. package/src/__tests__/avatar-e2e.test.ts +452 -0
  15. package/src/__tests__/avatar-generator.test.ts +193 -0
  16. package/src/__tests__/avatar-router.test.ts +186 -0
  17. package/src/__tests__/browser-download-timeout.test.ts +28 -0
  18. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
  19. package/src/__tests__/call-domain.test.ts +3 -7
  20. package/src/__tests__/credential-security-e2e.test.ts +19 -12
  21. package/src/__tests__/credentials-cli.test.ts +30 -4
  22. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
  23. package/src/__tests__/handlers-slack-config.test.ts +0 -72
  24. package/src/__tests__/handlers-telegram-config.test.ts +19 -12
  25. package/src/__tests__/handlers-twitter-config.test.ts +105 -48
  26. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  27. package/src/__tests__/integration-status.test.ts +15 -5
  28. package/src/__tests__/integrations-cli.test.ts +1 -1
  29. package/src/__tests__/invite-redemption-service.test.ts +62 -7
  30. package/src/__tests__/ipc-snapshot.test.ts +0 -8
  31. package/src/__tests__/managed-avatar-client.test.ts +280 -0
  32. package/src/__tests__/mcp-cli.test.ts +3 -3
  33. package/src/__tests__/oauth-cli.test.ts +203 -0
  34. package/src/__tests__/relay-server.test.ts +3 -3
  35. package/src/__tests__/secret-onetime-send.test.ts +19 -12
  36. package/src/__tests__/secure-keys.test.ts +78 -0
  37. package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
  38. package/src/__tests__/slack-channel-config.test.ts +23 -16
  39. package/src/__tests__/slack-share-routes.test.ts +263 -0
  40. package/src/__tests__/sms-messaging-provider.test.ts +3 -1
  41. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
  42. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  43. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  44. package/src/__tests__/twilio-config.test.ts +15 -36
  45. package/src/__tests__/twilio-provider.test.ts +4 -0
  46. package/src/__tests__/twitter-auth-handler.test.ts +27 -14
  47. package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
  48. package/src/__tests__/twitter-cli-routing.test.ts +38 -53
  49. package/src/__tests__/twitter-oauth-client.test.ts +18 -47
  50. package/src/__tests__/voice-invite-redemption.test.ts +27 -3
  51. package/src/amazon/cart.ts +1 -1
  52. package/src/amazon/client.ts +89 -7
  53. package/src/approvals/guardian-request-resolvers.ts +2 -2
  54. package/src/bundler/app-bundler.ts +77 -32
  55. package/src/bundler/app-compiler.ts +195 -0
  56. package/src/bundler/manifest.ts +1 -1
  57. package/src/bundler/package-resolver.ts +185 -0
  58. package/src/calls/call-domain.ts +4 -14
  59. package/src/calls/relay-server.ts +2 -2
  60. package/src/calls/twilio-config.ts +5 -24
  61. package/src/calls/twilio-rest.ts +19 -5
  62. package/src/cli/amazon.ts +74 -249
  63. package/src/cli/audit.ts +2 -2
  64. package/src/cli/autonomy.ts +9 -9
  65. package/src/cli/channels.ts +5 -5
  66. package/src/cli/completions.ts +27 -27
  67. package/src/cli/config.ts +14 -14
  68. package/src/cli/contacts.ts +27 -27
  69. package/src/cli/credentials.ts +28 -28
  70. package/src/cli/dev.ts +2 -2
  71. package/src/cli/doctor.ts +2 -2
  72. package/src/cli/email.ts +82 -82
  73. package/src/cli/influencer.ts +13 -13
  74. package/src/cli/integrations.ts +19 -144
  75. package/src/cli/keys.ts +10 -10
  76. package/src/cli/map.ts +4 -4
  77. package/src/cli/mcp.ts +17 -17
  78. package/src/cli/memory.ts +18 -18
  79. package/src/cli/notifications.ts +13 -13
  80. package/src/cli/oauth.ts +77 -0
  81. package/src/cli/program.ts +2 -0
  82. package/src/cli/sequence.ts +27 -27
  83. package/src/cli/sessions.ts +12 -12
  84. package/src/cli/trust.ts +8 -8
  85. package/src/cli/twitter.ts +124 -70
  86. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  87. package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
  88. package/src/config/bundled-skills/amazon/SKILL.md +54 -54
  89. package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
  90. package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
  91. package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
  92. package/src/config/bundled-skills/contacts/SKILL.md +12 -12
  93. package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
  94. package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
  95. package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
  96. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
  97. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
  98. package/src/config/bundled-skills/influencer/SKILL.md +13 -13
  99. package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
  101. package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
  102. package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
  103. package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
  104. package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
  105. package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
  106. package/src/config/bundled-skills/twitter/SKILL.md +68 -44
  107. package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
  108. package/src/config/core-schema.ts +26 -0
  109. package/src/config/env.ts +4 -0
  110. package/src/config/feature-flag-registry.json +9 -1
  111. package/src/config/schema.ts +8 -0
  112. package/src/config/system-prompt.ts +6 -3
  113. package/src/config/templates/BOOTSTRAP.md +7 -5
  114. package/src/contacts/contacts-write.ts +5 -1
  115. package/src/daemon/handlers/apps.ts +31 -4
  116. package/src/daemon/handlers/config-ingress.ts +3 -3
  117. package/src/daemon/handlers/config-integrations.ts +120 -49
  118. package/src/daemon/handlers/config-slack-channel.ts +26 -7
  119. package/src/daemon/handlers/config-slack.ts +1 -54
  120. package/src/daemon/handlers/config-telegram.ts +28 -10
  121. package/src/daemon/handlers/config.ts +1 -4
  122. package/src/daemon/handlers/twitter-auth.ts +11 -4
  123. package/src/daemon/ipc-contract/apps.ts +0 -13
  124. package/src/daemon/ipc-contract-inventory.json +0 -2
  125. package/src/daemon/lifecycle.ts +8 -1
  126. package/src/daemon/session-messaging.ts +2 -2
  127. package/src/daemon/tool-side-effects.ts +30 -0
  128. package/src/email/providers/agentmail.ts +1 -1
  129. package/src/email/providers/index.ts +1 -1
  130. package/src/email/service.ts +1 -1
  131. package/src/gallery/default-gallery.ts +538 -0
  132. package/src/gallery/gallery-manifest.ts +5 -1
  133. package/src/influencer/client.ts +8 -6
  134. package/src/mcp/client.ts +1 -1
  135. package/src/media/avatar-router.ts +99 -0
  136. package/src/media/avatar-types.ts +60 -0
  137. package/src/media/managed-avatar-client.ts +189 -0
  138. package/src/memory/app-migration.ts +114 -0
  139. package/src/memory/app-store.ts +11 -0
  140. package/src/memory/qdrant-client.ts +1 -1
  141. package/src/messaging/providers/slack/client.ts +12 -2
  142. package/src/messaging/providers/sms/adapter.ts +6 -10
  143. package/src/migrations/data-layout.ts +8 -1
  144. package/src/oauth/token-persistence.ts +9 -6
  145. package/src/runtime/assistant-scope.ts +5 -0
  146. package/src/runtime/auth/route-policy.ts +4 -0
  147. package/src/runtime/channel-readiness-service.ts +9 -4
  148. package/src/runtime/gateway-internal-client.ts +11 -3
  149. package/src/runtime/http-server.ts +2 -0
  150. package/src/runtime/invite-redemption-service.ts +23 -13
  151. package/src/runtime/middleware/twilio-validation.ts +2 -2
  152. package/src/runtime/routes/app-routes.ts +131 -3
  153. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
  154. package/src/runtime/routes/integration-routes.ts +2 -2
  155. package/src/runtime/routes/slack-share-routes.ts +235 -0
  156. package/src/runtime/routes/twilio-routes.ts +47 -34
  157. package/src/schedule/integration-status.ts +2 -3
  158. package/src/security/token-manager.ts +11 -3
  159. package/src/tools/apps/executors.ts +116 -8
  160. package/src/tools/browser/browser-manager.ts +30 -2
  161. package/src/tools/browser/chrome-cdp.ts +31 -3
  162. package/src/tools/credentials/vault.ts +9 -7
  163. package/src/tools/executor.ts +4 -0
  164. package/src/tools/system/avatar-generator.ts +55 -34
  165. package/src/twitter/client.ts +1 -1
  166. package/src/twitter/oauth-client.ts +31 -43
  167. package/src/twitter/router.ts +25 -23
  168. package/src/util/platform.ts +5 -0
  169. package/src/slack/slack-webhook.ts +0 -66
@@ -0,0 +1,186 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mock state
5
+ // ---------------------------------------------------------------------------
6
+
7
+ let mockStrategy: string | undefined;
8
+ let mockGeminiKey: string | undefined = "test-gemini-key";
9
+ let mockManagedAvailable = true;
10
+ let mockManagedResult: unknown;
11
+ let mockManagedError: Error | undefined;
12
+ let mockGeminiResult: unknown;
13
+
14
+ const generateManagedAvatarFn = mock(async () => {
15
+ if (mockManagedError) throw mockManagedError;
16
+ return mockManagedResult;
17
+ });
18
+
19
+ const generateImageFn = mock(async () => mockGeminiResult);
20
+
21
+ const isManagedAvailableFn = mock(() => mockManagedAvailable);
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Mock modules — before importing module under test
25
+ // ---------------------------------------------------------------------------
26
+
27
+ mock.module("../config/loader.js", () => ({
28
+ getConfig: () => ({
29
+ apiKeys: { gemini: mockGeminiKey },
30
+ avatar: { generationStrategy: mockStrategy ?? "local_only" },
31
+ }),
32
+ }));
33
+
34
+ mock.module("../util/logger.js", () => ({
35
+ getLogger: () => ({
36
+ debug: () => {},
37
+ info: () => {},
38
+ warn: () => {},
39
+ error: () => {},
40
+ }),
41
+ }));
42
+
43
+ mock.module("../media/managed-avatar-client.js", () => ({
44
+ isManagedAvailable: isManagedAvailableFn,
45
+ generateManagedAvatar: generateManagedAvatarFn,
46
+ }));
47
+
48
+ mock.module("../media/gemini-image-service.js", () => ({
49
+ generateImage: generateImageFn,
50
+ }));
51
+
52
+ // Import after mocking
53
+ import {
54
+ getAvatarStrategy,
55
+ routedGenerateAvatar,
56
+ } from "../media/avatar-router.js";
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Helpers
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function managedResponse() {
63
+ return {
64
+ image: {
65
+ mime_type: "image/png",
66
+ data_base64: "base64data",
67
+ bytes: 1024,
68
+ sha256: "abc",
69
+ },
70
+ usage: { billable: false, class_name: "assistant_avatar_system" },
71
+ generation_source: "vertex",
72
+ profile: "avatar_v1",
73
+ correlation_id: "test-correlation-id",
74
+ };
75
+ }
76
+
77
+ function geminiResponse() {
78
+ return {
79
+ images: [{ mimeType: "image/png", dataBase64: "base64data" }],
80
+ resolvedModel: "gemini-2.5-flash-image",
81
+ };
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Tests
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe("avatar-router", () => {
89
+ beforeEach(() => {
90
+ mockStrategy = undefined;
91
+ mockGeminiKey = "test-gemini-key";
92
+ mockManagedAvailable = true;
93
+ mockManagedResult = managedResponse();
94
+ mockManagedError = undefined;
95
+ mockGeminiResult = geminiResponse();
96
+ generateManagedAvatarFn.mockClear();
97
+ generateImageFn.mockClear();
98
+ isManagedAvailableFn.mockClear();
99
+ });
100
+
101
+ // 1. managed_required — managed success
102
+ test("managed_required returns pathUsed managed on success", async () => {
103
+ mockStrategy = "managed_required";
104
+ const result = await routedGenerateAvatar("a cute cat");
105
+ expect(result.pathUsed).toBe("managed");
106
+ expect(result.imageBase64).toBe("base64data");
107
+ expect(result.mimeType).toBe("image/png");
108
+ expect(generateManagedAvatarFn).toHaveBeenCalledTimes(1);
109
+ expect(generateImageFn).not.toHaveBeenCalled();
110
+ });
111
+
112
+ // 2. managed_required — managed failure throws, no fallback
113
+ test("managed_required throws on managed failure without fallback", async () => {
114
+ mockStrategy = "managed_required";
115
+ mockManagedError = new Error("upstream error");
116
+ await expect(routedGenerateAvatar("a cute cat")).rejects.toThrow(
117
+ "upstream error",
118
+ );
119
+ expect(generateImageFn).not.toHaveBeenCalled();
120
+ });
121
+
122
+ // 3. local_only — calls local Gemini, never managed
123
+ test("local_only calls local Gemini and never managed", async () => {
124
+ mockStrategy = "local_only";
125
+ const result = await routedGenerateAvatar("a cute cat");
126
+ expect(result.pathUsed).toBe("local");
127
+ expect(result.imageBase64).toBe("base64data");
128
+ expect(generateImageFn).toHaveBeenCalledTimes(1);
129
+ expect(generateManagedAvatarFn).not.toHaveBeenCalled();
130
+ });
131
+
132
+ // 4. local_only — missing Gemini API key throws
133
+ test("local_only throws when Gemini API key is missing", async () => {
134
+ mockStrategy = "local_only";
135
+ mockGeminiKey = undefined;
136
+ // Also clear the env var to ensure no fallback
137
+ const saved = process.env.GEMINI_API_KEY;
138
+ delete process.env.GEMINI_API_KEY;
139
+ try {
140
+ await expect(routedGenerateAvatar("a cute cat")).rejects.toThrow(
141
+ "Gemini API key is not configured",
142
+ );
143
+ expect(generateImageFn).not.toHaveBeenCalled();
144
+ } finally {
145
+ if (saved !== undefined) process.env.GEMINI_API_KEY = saved;
146
+ }
147
+ });
148
+
149
+ // 5. managed_prefer — managed success
150
+ test("managed_prefer returns pathUsed managed on success", async () => {
151
+ mockStrategy = "managed_prefer";
152
+ const result = await routedGenerateAvatar("a cute cat");
153
+ expect(result.pathUsed).toBe("managed");
154
+ expect(generateManagedAvatarFn).toHaveBeenCalledTimes(1);
155
+ expect(generateImageFn).not.toHaveBeenCalled();
156
+ });
157
+
158
+ // 6. managed_prefer — managed failure falls back to local
159
+ test("managed_prefer falls back to local on managed failure", async () => {
160
+ mockStrategy = "managed_prefer";
161
+ mockManagedError = new Error("managed failed");
162
+ const result = await routedGenerateAvatar("a cute cat");
163
+ expect(result.pathUsed).toBe("local");
164
+ expect(generateManagedAvatarFn).toHaveBeenCalledTimes(1);
165
+ expect(generateImageFn).toHaveBeenCalledTimes(1);
166
+ });
167
+
168
+ // 7. managed_prefer — managed unavailable goes directly to local
169
+ test("managed_prefer goes to local when managed unavailable", async () => {
170
+ mockStrategy = "managed_prefer";
171
+ mockManagedAvailable = false;
172
+ const result = await routedGenerateAvatar("a cute cat");
173
+ expect(result.pathUsed).toBe("local");
174
+ expect(generateManagedAvatarFn).not.toHaveBeenCalled();
175
+ expect(generateImageFn).toHaveBeenCalledTimes(1);
176
+ });
177
+
178
+ // 8. Default strategy is local_only when config key absent
179
+ test("defaults to local_only when config key is absent", () => {
180
+ mockStrategy = undefined;
181
+ expect(getAvatarStrategy()).toBe("local_only");
182
+ });
183
+
184
+ // 9. Removed: Invalid strategy values are now rejected at config parse time
185
+ // by the Zod schema, so they cannot reach getAvatarStrategy().
186
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { withTimeout } from "../tools/browser/browser-manager.js";
4
+
5
+ describe("withTimeout", () => {
6
+ it("resolves normally for a fast promise", async () => {
7
+ const result = await withTimeout(Promise.resolve("ok"), 1_000, "fast");
8
+ expect(result).toBe("ok");
9
+ });
10
+
11
+ it("rejects with timeout error for a hanging promise", async () => {
12
+ const hanging = new Promise<string>(() => {
13
+ // never resolves
14
+ });
15
+
16
+ await expect(withTimeout(hanging, 50, "slow op")).rejects.toThrow(
17
+ "slow op timed out after 50ms",
18
+ );
19
+ });
20
+
21
+ it("propagates the original rejection if it happens before the timeout", async () => {
22
+ const failing = Promise.reject(new Error("original error"));
23
+
24
+ await expect(withTimeout(failing, 1_000, "failing")).rejects.toThrow(
25
+ "original error",
26
+ );
27
+ });
28
+ });
@@ -90,16 +90,16 @@ const GATEWAY_RETRIEVAL_BANLIST: Array<{
90
90
  {
91
91
  skillPath: "voice-setup/SKILL.md",
92
92
  bannedSnippets: [
93
- "vellum config get elevenlabs.voiceId",
94
- "vellum config get calls.enabled",
93
+ "assistant config get elevenlabs.voiceId",
94
+ "assistant config get calls.enabled",
95
95
  ],
96
96
  },
97
97
  {
98
98
  skillPath: "email-setup/SKILL.md",
99
99
  bannedSnippets: [
100
100
  "host_bash",
101
- "vellum email create",
102
- "vellum config set email.address",
101
+ "assistant email create",
102
+ "assistant config set email.address",
103
103
  ],
104
104
  },
105
105
  ];
@@ -118,11 +118,11 @@ const HOST_BASH_RETRIEVAL_ALLOWLIST = new Set<string>([
118
118
  ]);
119
119
 
120
120
  const RETRIEVAL_MARKERS = [
121
- "vellum integrations ",
122
- "vellum config get",
123
- "vellum email status",
124
- "vellum email inbox list",
125
- "vellum email provider get",
121
+ "assistant integrations ",
122
+ "assistant config get",
123
+ "assistant email status",
124
+ "assistant email inbox list",
125
+ "assistant email provider get",
126
126
  ];
127
127
 
128
128
  describe("bundled skill retrieval guard", () => {
@@ -207,16 +207,12 @@ describe("resolveCallerIdentity — strict implicit-default policy", () => {
207
207
  }
208
208
  });
209
209
 
210
- test("assistant_number resolves from assistant-scoped Twilio number when assistantId is provided", async () => {
211
- const result = await resolveCallerIdentity(
212
- makeConfig(),
213
- undefined,
214
- "ast-alpha",
215
- );
210
+ test("assistant_number resolves from twilio config phone number", async () => {
211
+ const result = await resolveCallerIdentity(makeConfig());
216
212
  expect(result.ok).toBe(true);
217
213
  if (result.ok) {
218
214
  expect(result.mode).toBe("assistant_number");
219
- expect(result.fromNumber).toBe("+15550003333");
215
+ expect(result.fromNumber).toBe("+15550001111");
220
216
  expect(result.source).toBe("implicit_default");
221
217
  }
222
218
  });
@@ -30,23 +30,30 @@ mock.module("../util/logger.js", () => ({
30
30
 
31
31
  // Track keychain writes
32
32
  const storedKeys = new Map<string, string>();
33
- mock.module("../security/secure-keys.js", () => ({
34
- getSecureKey: (key: string) => storedKeys.get(key) ?? null,
35
- setSecureKey: (key: string, value: string) => {
33
+ mock.module("../security/secure-keys.js", () => {
34
+ const syncSet = (key: string, value: string) => {
36
35
  storedKeys.set(key, value);
37
36
  return true;
38
- },
39
- deleteSecureKey: (key: string) => {
37
+ };
38
+ const syncDelete = (key: string) => {
40
39
  if (storedKeys.has(key)) {
41
40
  storedKeys.delete(key);
42
- return "deleted";
41
+ return "deleted" as const;
43
42
  }
44
- return "not-found";
45
- },
46
- listSecureKeys: () => [...storedKeys.keys()],
47
- getBackendType: () => "encrypted",
48
- isDowngradedFromKeychain: () => false,
49
- }));
43
+ return "not-found" as const;
44
+ };
45
+ return {
46
+ getSecureKey: (key: string) => storedKeys.get(key) ?? null,
47
+ setSecureKey: syncSet,
48
+ setSecureKeyAsync: async (key: string, value: string) =>
49
+ syncSet(key, value),
50
+ deleteSecureKey: syncDelete,
51
+ deleteSecureKeyAsync: async (key: string) => syncDelete(key),
52
+ listSecureKeys: () => [...storedKeys.keys()],
53
+ getBackendType: () => "encrypted",
54
+ isDowngradedFromKeychain: () => false,
55
+ };
56
+ });
50
57
 
51
58
  // In-memory metadata store that mirrors storedKeys for list/get operations
52
59
  const metadataStore = new Map<
@@ -39,17 +39,43 @@ mock.module("../security/secure-keys.js", () => ({
39
39
  secureKeyStore.set(account, value);
40
40
  return true;
41
41
  },
42
- deleteSecureKey: (account: string) => {
42
+ setSecureKeyAsync: async (
43
+ account: string,
44
+ value: string,
45
+ ): Promise<boolean> => {
46
+ _setSecureKeyCalls += 1;
47
+ secureKeyStore.set(account, value);
48
+ return true;
49
+ },
50
+ deleteSecureKey: (account: string): "deleted" | "not-found" | "error" => {
51
+ _deleteSecureKeyCalls += 1;
52
+ if (secureKeyStore.has(account)) {
53
+ secureKeyStore.delete(account);
54
+ return "deleted";
55
+ }
56
+ return "not-found";
57
+ },
58
+ deleteSecureKeyAsync: async (
59
+ account: string,
60
+ ): Promise<"deleted" | "not-found" | "error"> => {
43
61
  _deleteSecureKeyCalls += 1;
44
62
  if (secureKeyStore.has(account)) {
45
63
  secureKeyStore.delete(account);
46
- return "deleted" as const;
64
+ return "deleted";
47
65
  }
48
- return "not-found" as const;
66
+ return "not-found";
49
67
  },
50
68
  listSecureKeys: (): string[] => {
51
69
  return [...secureKeyStore.keys()];
52
70
  },
71
+ getSecureKeyAsync: async (account: string): Promise<string | undefined> => {
72
+ _getSecureKeyCalls += 1;
73
+ return secureKeyStore.get(account);
74
+ },
75
+ getBackendType: (): "broker" | "encrypted" | null => null,
76
+ isDowngradedFromKeychain: (): boolean => false,
77
+ _resetBackend: (): void => {},
78
+ _setBackend: (): void => {},
53
79
  }));
54
80
 
55
81
  // ---------------------------------------------------------------------------
@@ -243,7 +269,7 @@ function seedMetadataOnly(
243
269
  // Tests
244
270
  // ---------------------------------------------------------------------------
245
271
 
246
- describe("vellum credentials CLI", () => {
272
+ describe("assistant credentials CLI", () => {
247
273
  beforeEach(() => {
248
274
  secureKeyStore = new Map();
249
275
  metadataStore = [];
@@ -55,7 +55,7 @@ describe("guardian-verify-setup skill — voice auto-followup", () => {
55
55
  .split("## Voice Auto-Check Polling")[1]
56
56
  ?.split("## Step 6")[0] ?? "";
57
57
  expect(pollingSection).toContain(
58
- "vellum integrations guardian status --channel voice --json",
58
+ "assistant integrations guardian status --channel voice --json",
59
59
  );
60
60
  });
61
61
 
@@ -55,33 +55,9 @@ mock.module("../util/logger.js", () => ({
55
55
  }),
56
56
  }));
57
57
 
58
- // Mock app-store so getApp returns a fake app for share tests
59
- mock.module("../memory/app-store.js", () => ({
60
- queryAppRecords: () => [],
61
- createAppRecord: () => {},
62
- updateAppRecord: () => {},
63
- deleteAppRecord: () => {},
64
- listApps: () => [],
65
- getApp: (id: string) =>
66
- id === "test-app"
67
- ? { id: "test-app", name: "Test App", description: "A test app" }
68
- : undefined,
69
- createApp: () => {},
70
- updateApp: () => {},
71
- }));
72
-
73
- // Mock Slack webhook poster
74
- const postedWebhooks: { url: string; name: string }[] = [];
75
- mock.module("../slack/slack-webhook.js", () => ({
76
- postToSlackWebhook: async (url: string, name: string) => {
77
- postedWebhooks.push({ url, name });
78
- },
79
- }));
80
-
81
58
  import { handleMessage, type HandlerContext } from "../daemon/handlers.js";
82
59
  import type {
83
60
  ServerMessage,
84
- ShareToSlackRequest,
85
61
  SlackWebhookConfigRequest,
86
62
  } from "../daemon/ipc-contract.js";
87
63
  import { DebouncerMap } from "../util/debounce.js";
@@ -161,52 +137,4 @@ describe("Slack handlers use workspace config (not hardcoded path)", () => {
161
137
  "https://hooks.slack.com/new",
162
138
  );
163
139
  });
164
-
165
- test("share_to_slack reads webhook URL from loadRawConfig", async () => {
166
- rawConfigStore = { slackWebhookUrl: "https://hooks.slack.com/share" };
167
- postedWebhooks.length = 0;
168
-
169
- const msg: ShareToSlackRequest = {
170
- type: "share_to_slack",
171
- appId: "test-app",
172
- };
173
-
174
- const { ctx, sent } = createTestContext();
175
- await handleMessage(msg, {} as net.Socket, ctx);
176
-
177
- // Wait a tick for async handler to complete
178
- await new Promise((r) => setTimeout(r, 50));
179
-
180
- const res = sent.find(
181
- (m) => (m as { type: string }).type === "share_to_slack_response",
182
- ) as { type: string; success: boolean } | undefined;
183
- expect(res).toBeDefined();
184
- expect(res!.success).toBe(true);
185
-
186
- // Verify the webhook was posted with the URL from loadRawConfig
187
- expect(postedWebhooks).toHaveLength(1);
188
- expect(postedWebhooks[0]!.url).toBe("https://hooks.slack.com/share");
189
- expect(postedWebhooks[0]!.name).toBe("Test App");
190
- });
191
-
192
- test("share_to_slack fails gracefully when no webhook URL configured", async () => {
193
- rawConfigStore = {};
194
-
195
- const msg: ShareToSlackRequest = {
196
- type: "share_to_slack",
197
- appId: "test-app",
198
- };
199
-
200
- const { ctx, sent } = createTestContext();
201
- await handleMessage(msg, {} as net.Socket, ctx);
202
-
203
- await new Promise((r) => setTimeout(r, 50));
204
-
205
- const res = sent.find(
206
- (m) => (m as { type: string }).type === "share_to_slack_response",
207
- ) as { type: string; success: boolean; error?: string } | undefined;
208
- expect(res).toBeDefined();
209
- expect(res!.success).toBe(false);
210
- expect(res!.error).toContain("No Slack webhook URL configured");
211
- });
212
140
  });
@@ -66,20 +66,27 @@ let secureKeyStore: Record<string, string> = {};
66
66
  let setSecureKeyOverride: ((account: string, value: string) => boolean) | null =
67
67
  null;
68
68
 
69
+ function syncSet(account: string, value: string): boolean {
70
+ if (setSecureKeyOverride) return setSecureKeyOverride(account, value);
71
+ secureKeyStore[account] = value;
72
+ return true;
73
+ }
74
+
75
+ function syncDelete(account: string): "deleted" | "not-found" {
76
+ if (account in secureKeyStore) {
77
+ delete secureKeyStore[account];
78
+ return "deleted";
79
+ }
80
+ return "not-found";
81
+ }
82
+
69
83
  mock.module("../security/secure-keys.js", () => ({
70
84
  getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
71
- setSecureKey: (account: string, value: string) => {
72
- if (setSecureKeyOverride) return setSecureKeyOverride(account, value);
73
- secureKeyStore[account] = value;
74
- return true;
75
- },
76
- deleteSecureKey: (account: string) => {
77
- if (account in secureKeyStore) {
78
- delete secureKeyStore[account];
79
- return "deleted";
80
- }
81
- return "not-found";
82
- },
85
+ setSecureKey: syncSet,
86
+ deleteSecureKey: syncDelete,
87
+ setSecureKeyAsync: async (account: string, value: string) =>
88
+ syncSet(account, value),
89
+ deleteSecureKeyAsync: async (account: string) => syncDelete(account),
83
90
  listSecureKeys: () => Object.keys(secureKeyStore),
84
91
  getBackendType: () => "encrypted",
85
92
  isDowngradedFromKeychain: () => false,