@vellumai/assistant 0.4.52 → 0.4.53

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 (205) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/keychain-broker.md +6 -20
  3. package/docs/architecture/memory.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/approval-cascade.test.ts +3 -1
  6. package/src/__tests__/approval-routes-http.test.ts +0 -1
  7. package/src/__tests__/asset-materialize-tool.test.ts +0 -1
  8. package/src/__tests__/asset-search-tool.test.ts +0 -1
  9. package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
  10. package/src/__tests__/attachments-store.test.ts +0 -1
  11. package/src/__tests__/avatar-e2e.test.ts +6 -1
  12. package/src/__tests__/browser-fill-credential.test.ts +3 -0
  13. package/src/__tests__/btw-routes.test.ts +39 -0
  14. package/src/__tests__/call-controller.test.ts +0 -1
  15. package/src/__tests__/call-domain.test.ts +1 -0
  16. package/src/__tests__/call-routes-http.test.ts +1 -2
  17. package/src/__tests__/canonical-guardian-store.test.ts +33 -2
  18. package/src/__tests__/channel-readiness-service.test.ts +1 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +6 -2
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +7 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +1 -2
  22. package/src/__tests__/config-schema.test.ts +6 -37
  23. package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
  24. package/src/__tests__/credential-broker-server-use.test.ts +16 -16
  25. package/src/__tests__/credential-security-invariants.test.ts +14 -0
  26. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  27. package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
  28. package/src/__tests__/gateway-only-enforcement.test.ts +0 -2
  29. package/src/__tests__/host-shell-tool.test.ts +0 -1
  30. package/src/__tests__/http-user-message-parity.test.ts +19 -0
  31. package/src/__tests__/list-messages-attachments.test.ts +0 -1
  32. package/src/__tests__/log-export-workspace.test.ts +233 -0
  33. package/src/__tests__/managed-proxy-context.test.ts +1 -1
  34. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  35. package/src/__tests__/media-generate-image.test.ts +7 -2
  36. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -1
  37. package/src/__tests__/memory-regressions.test.ts +0 -1
  38. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  39. package/src/__tests__/migration-export-http.test.ts +0 -1
  40. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  41. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  42. package/src/__tests__/migration-validate-http.test.ts +0 -1
  43. package/src/__tests__/notification-schedule-dedup.test.ts +237 -0
  44. package/src/__tests__/oauth-cli.test.ts +1 -10
  45. package/src/__tests__/oauth-store.test.ts +3 -5
  46. package/src/__tests__/oauth2-gateway-transport.test.ts +5 -4
  47. package/src/__tests__/onboarding-starter-tasks.test.ts +1 -1
  48. package/src/__tests__/onboarding-template-contract.test.ts +1 -2
  49. package/src/__tests__/pricing.test.ts +0 -11
  50. package/src/__tests__/provider-commit-message-generator.test.ts +21 -14
  51. package/src/__tests__/provider-fail-open-selection.test.ts +9 -8
  52. package/src/__tests__/provider-managed-proxy-integration.test.ts +27 -24
  53. package/src/__tests__/provider-registry-ollama.test.ts +8 -2
  54. package/src/__tests__/recording-handler.test.ts +0 -1
  55. package/src/__tests__/relay-server.test.ts +0 -1
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  57. package/src/__tests__/runtime-events-sse-parity.test.ts +0 -1
  58. package/src/__tests__/runtime-events-sse.test.ts +0 -1
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -1
  60. package/src/__tests__/secret-scanner-executor.test.ts +0 -1
  61. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  62. package/src/__tests__/session-abort-tool-results.test.ts +3 -1
  63. package/src/__tests__/session-agent-loop-overflow.test.ts +1012 -838
  64. package/src/__tests__/session-agent-loop.test.ts +2 -2
  65. package/src/__tests__/session-confirmation-signals.test.ts +3 -1
  66. package/src/__tests__/session-error.test.ts +5 -4
  67. package/src/__tests__/session-history-web-search.test.ts +34 -9
  68. package/src/__tests__/session-pre-run-repair.test.ts +3 -1
  69. package/src/__tests__/session-provider-retry-repair.test.ts +31 -26
  70. package/src/__tests__/session-queue.test.ts +3 -1
  71. package/src/__tests__/session-runtime-assembly.test.ts +118 -0
  72. package/src/__tests__/session-slash-known.test.ts +31 -13
  73. package/src/__tests__/session-slash-queue.test.ts +3 -1
  74. package/src/__tests__/session-slash-unknown.test.ts +3 -1
  75. package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
  76. package/src/__tests__/session-workspace-injection.test.ts +3 -1
  77. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -1
  78. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  79. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  80. package/src/__tests__/skillssh-registry.test.ts +21 -0
  81. package/src/__tests__/slack-share-routes.test.ts +1 -1
  82. package/src/__tests__/swarm-recursion.test.ts +5 -1
  83. package/src/__tests__/swarm-session-integration.test.ts +25 -14
  84. package/src/__tests__/swarm-tool.test.ts +5 -2
  85. package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
  86. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1521 -0
  87. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/trust-store.test.ts +5 -1
  92. package/src/__tests__/twilio-routes.test.ts +2 -2
  93. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  94. package/src/__tests__/voice-quality.test.ts +2 -1
  95. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  96. package/src/__tests__/web-search.test.ts +1 -1
  97. package/src/agent/loop.ts +17 -1
  98. package/src/bundler/app-bundler.ts +40 -24
  99. package/src/calls/call-controller.ts +16 -0
  100. package/src/calls/relay-server.ts +29 -13
  101. package/src/calls/voice-control-protocol.ts +1 -0
  102. package/src/calls/voice-quality.ts +1 -1
  103. package/src/calls/voice-session-bridge.ts +9 -3
  104. package/src/channels/types.ts +16 -0
  105. package/src/cli/commands/bash.ts +173 -0
  106. package/src/cli/commands/doctor.ts +5 -23
  107. package/src/cli/commands/oauth/connections.ts +4 -2
  108. package/src/cli/commands/oauth/providers.ts +1 -13
  109. package/src/cli/program.ts +2 -0
  110. package/src/cli/reference.ts +1 -0
  111. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -1
  112. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +3 -5
  113. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -3
  114. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +1 -1
  115. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
  116. package/src/config/feature-flag-registry.json +8 -0
  117. package/src/config/loader.ts +7 -135
  118. package/src/config/schema.ts +0 -6
  119. package/src/config/schemas/channels.ts +1 -0
  120. package/src/config/schemas/elevenlabs.ts +2 -2
  121. package/src/contacts/contact-store.ts +21 -25
  122. package/src/contacts/contacts-write.ts +6 -6
  123. package/src/contacts/types.ts +2 -0
  124. package/src/context/token-estimator.ts +35 -2
  125. package/src/context/window-manager.ts +16 -2
  126. package/src/daemon/config-watcher.ts +24 -6
  127. package/src/daemon/context-overflow-reducer.ts +13 -2
  128. package/src/daemon/handlers/config-ingress.ts +25 -8
  129. package/src/daemon/handlers/config-model.ts +21 -15
  130. package/src/daemon/handlers/config-telegram.ts +18 -6
  131. package/src/daemon/handlers/dictation.ts +0 -429
  132. package/src/daemon/handlers/skills.ts +1 -200
  133. package/src/daemon/lifecycle.ts +8 -5
  134. package/src/daemon/message-types/contacts.ts +2 -0
  135. package/src/daemon/message-types/integrations.ts +1 -0
  136. package/src/daemon/message-types/sessions.ts +2 -0
  137. package/src/daemon/parse-actual-tokens-from-error.test.ts +75 -0
  138. package/src/daemon/server.ts +23 -2
  139. package/src/daemon/session-agent-loop-handlers.ts +1 -1
  140. package/src/daemon/session-agent-loop.ts +27 -79
  141. package/src/daemon/session-error.ts +5 -4
  142. package/src/daemon/session-process.ts +17 -10
  143. package/src/daemon/session-runtime-assembly.ts +50 -0
  144. package/src/daemon/session-slash.ts +32 -20
  145. package/src/daemon/session.ts +1 -0
  146. package/src/events/domain-events.ts +1 -0
  147. package/src/media/app-icon-generator.ts +2 -1
  148. package/src/media/avatar-router.ts +3 -2
  149. package/src/memory/canonical-guardian-store.ts +25 -3
  150. package/src/memory/db-init.ts +12 -0
  151. package/src/memory/embedding-backend.ts +25 -16
  152. package/src/memory/migrations/158-channel-interaction-columns.ts +18 -0
  153. package/src/memory/migrations/159-drop-contact-interaction-columns.ts +16 -0
  154. package/src/memory/migrations/160-drop-loopback-port-column.ts +13 -0
  155. package/src/memory/migrations/index.ts +3 -0
  156. package/src/memory/retriever.test.ts +19 -12
  157. package/src/memory/schema/contacts.ts +2 -2
  158. package/src/memory/schema/oauth.ts +0 -1
  159. package/src/oauth/connect-orchestrator.ts +5 -3
  160. package/src/oauth/connect-types.ts +9 -2
  161. package/src/oauth/manual-token-connection.ts +9 -7
  162. package/src/oauth/oauth-store.ts +2 -8
  163. package/src/oauth/provider-behaviors.ts +10 -0
  164. package/src/oauth/seed-providers.ts +13 -5
  165. package/src/permissions/checker.ts +20 -1
  166. package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
  167. package/src/prompts/system-prompt.ts +2 -11
  168. package/src/prompts/templates/BOOTSTRAP.md +1 -3
  169. package/src/providers/anthropic/client.ts +16 -8
  170. package/src/providers/managed-proxy/constants.ts +1 -1
  171. package/src/providers/registry.ts +21 -15
  172. package/src/providers/types.ts +1 -1
  173. package/src/runtime/auth/route-policy.ts +4 -0
  174. package/src/runtime/channel-invite-transports/telegram.ts +12 -6
  175. package/src/runtime/channel-retry-sweep.ts +6 -0
  176. package/src/runtime/http-types.ts +1 -0
  177. package/src/runtime/middleware/error-handler.ts +1 -2
  178. package/src/runtime/routes/app-management-routes.ts +1 -0
  179. package/src/runtime/routes/btw-routes.ts +20 -1
  180. package/src/runtime/routes/conversation-routes.ts +32 -13
  181. package/src/runtime/routes/inbound-message-handler.ts +10 -2
  182. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  183. package/src/runtime/routes/inbound-stages/edit-intercept.ts +5 -5
  184. package/src/runtime/routes/integrations/slack/share.ts +5 -5
  185. package/src/runtime/routes/log-export-routes.ts +122 -10
  186. package/src/runtime/routes/session-query-routes.ts +3 -3
  187. package/src/runtime/routes/settings-routes.ts +53 -0
  188. package/src/runtime/routes/workspace-routes.ts +3 -0
  189. package/src/runtime/verification-templates.ts +1 -1
  190. package/src/security/oauth2.ts +4 -4
  191. package/src/security/secure-keys.ts +4 -4
  192. package/src/signals/bash.ts +157 -0
  193. package/src/skills/skillssh-registry.ts +6 -1
  194. package/src/swarm/backend-claude-code.ts +6 -6
  195. package/src/swarm/worker-backend.ts +1 -1
  196. package/src/swarm/worker-runner.ts +1 -1
  197. package/src/telegram/bot-username.ts +11 -0
  198. package/src/tools/claude-code/claude-code.ts +4 -4
  199. package/src/tools/credentials/broker.ts +7 -5
  200. package/src/tools/credentials/vault.ts +3 -2
  201. package/src/tools/network/__tests__/web-search.test.ts +18 -86
  202. package/src/tools/network/web-search.ts +9 -15
  203. package/src/util/platform.ts +7 -1
  204. package/src/util/pricing.ts +0 -1
  205. package/src/workspace/provider-commit-message-generator.ts +10 -6
@@ -237,6 +237,20 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
237
237
  "cli/commands/oauth/connections.ts", // CLI OAuth connection delete (legacy credential cleanup)
238
238
  "oauth/manual-token-connection.ts", // manual-token provider backfill (keychain credential existence check)
239
239
  "cli/commands/doctor.ts", // CLI diagnostic API key verification via secure storage
240
+ "swarm/backend-claude-code.ts", // Claude Code swarm backend API key lookup
241
+ "workspace/provider-commit-message-generator.ts", // commit message generation provider key lookup
242
+ "config/bundled-skills/transcribe/tools/transcribe-media.ts", // transcription tool API key lookup
243
+ "config/bundled-skills/image-studio/tools/media-generate-image.ts", // image generation tool API key lookup
244
+ "config/bundled-skills/media-processing/tools/analyze-keyframes.ts", // keyframe analysis tool API key lookup
245
+ "config/bundled-skills/media-processing/tools/extract-keyframes.ts", // keyframe extraction tool API key lookup
246
+ "providers/registry.ts", // provider registry API key lookup for initialization
247
+ "media/app-icon-generator.ts", // app icon generation API key lookup
248
+ "media/avatar-router.ts", // avatar generation API key lookup
249
+ "memory/embedding-backend.ts", // embedding backend API key lookup
250
+ "daemon/handlers/config-model.ts", // model config handler API key lookup
251
+ "daemon/session-slash.ts", // session slash command API key lookup
252
+ "daemon/session-process.ts", // session process API key lookup
253
+ "tools/claude-code/claude-code.ts", // Claude Code tool API key lookup
240
254
  ]);
241
255
 
242
256
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -1113,7 +1113,7 @@ describe("CredentialBroker — serverUseById edge cases", () => {
1113
1113
  _resetBackend();
1114
1114
  });
1115
1115
 
1116
- test("serverUseById with multiple injection templates returns all", () => {
1116
+ test("serverUseById with multiple injection templates returns all", async () => {
1117
1117
  const meta = upsertCredentialMetadata("multi", "api_key", {
1118
1118
  allowedTools: ["proxy"],
1119
1119
  injectionTemplates: [
@@ -1132,7 +1132,7 @@ describe("CredentialBroker — serverUseById edge cases", () => {
1132
1132
  });
1133
1133
  setSecureKey(credentialKey("multi", "api_key"), "multi-secret");
1134
1134
 
1135
- const result = broker.serverUseById({
1135
+ const result = await broker.serverUseById({
1136
1136
  credentialId: meta.credentialId,
1137
1137
  requestingTool: "proxy",
1138
1138
  });
@@ -1146,13 +1146,13 @@ describe("CredentialBroker — serverUseById edge cases", () => {
1146
1146
  expect(JSON.stringify(result)).not.toContain("multi-secret");
1147
1147
  });
1148
1148
 
1149
- test("serverUseById verifies secret exists in storage (fail-closed)", () => {
1149
+ test("serverUseById verifies secret exists in storage (fail-closed)", async () => {
1150
1150
  const meta = upsertCredentialMetadata("fal", "api_key", {
1151
1151
  allowedTools: ["proxy"],
1152
1152
  });
1153
1153
  // No setSecureKey — metadata exists but value doesn't
1154
1154
 
1155
- const result = broker.serverUseById({
1155
+ const result = await broker.serverUseById({
1156
1156
  credentialId: meta.credentialId,
1157
1157
  requestingTool: "proxy",
1158
1158
  });
@@ -15,11 +15,10 @@ describe("withErrorHandling – friendly error messages", () => {
15
15
  };
16
16
  expect(body.error.code).toBe("UNPROCESSABLE_ENTITY");
17
17
  expect(body.error.message).toContain("No API key configured");
18
- expect(body.error.message).toContain("ANTHROPIC_API_KEY");
19
- expect(body.error.message).toContain("vellum hatch");
18
+ expect(body.error.message).toContain("keys set anthropic");
20
19
  });
21
20
 
22
- test("ProviderNotConfiguredError tailors env var to requested provider", async () => {
21
+ test("ProviderNotConfiguredError tailors keys set command to requested provider", async () => {
23
22
  const response = await withErrorHandling("test", async () => {
24
23
  throw new ProviderNotConfiguredError("openai", []);
25
24
  });
@@ -28,8 +27,8 @@ describe("withErrorHandling – friendly error messages", () => {
28
27
  const body = (await response.json()) as {
29
28
  error: { code: string; message: string };
30
29
  };
31
- expect(body.error.message).toContain("OPENAI_API_KEY");
32
- expect(body.error.message).not.toContain("ANTHROPIC_API_KEY");
30
+ expect(body.error.message).toContain("keys set openai");
31
+ expect(body.error.message).not.toContain("keys set anthropic");
33
32
  });
34
33
 
35
34
  test("generic ConfigError still returns its own message", async () => {
@@ -57,7 +57,6 @@ mock.module("../config/loader.js", () => ({
57
57
  loadConfig: () => ({
58
58
  model: "test",
59
59
  provider: "test",
60
- apiKeys: {},
61
60
  memory: { enabled: false },
62
61
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
63
62
  secretDetection: { enabled: false },
@@ -80,7 +79,6 @@ mock.module("../config/loader.js", () => ({
80
79
  getConfig: () => ({
81
80
  model: "test",
82
81
  provider: "test",
83
- apiKeys: {},
84
82
  memory: { enabled: false },
85
83
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
86
84
  secretDetection: { enabled: false },
@@ -22,7 +22,6 @@ mock.module("node:child_process", () => ({
22
22
  const mockConfig = {
23
23
  provider: "anthropic",
24
24
  model: "test",
25
- apiKeys: {},
26
25
  maxTokens: 4096,
27
26
  dataDir: "/tmp",
28
27
  timeouts: {
@@ -349,6 +349,25 @@ describe("HTTP POST /v1/messages does not intercept recording intents (by design
349
349
  expect(runAgentLoop).toHaveBeenCalledTimes(1);
350
350
  });
351
351
 
352
+ test("structured commandIntent recording actions are ignored by HTTP path", async () => {
353
+ // Even when the request includes a structured commandIntent for recording,
354
+ // the HTTP path does not parse or act on it — handleSendMessage only reads
355
+ // content, conversationKey, attachmentIds, sourceChannel, and interface.
356
+ // This ensures a future regression that starts parsing commandIntent would
357
+ // be caught.
358
+ const persistUserMessage = mock(async () => "persisted-msg-id");
359
+ const runAgentLoop = mock(async () => undefined);
360
+ const session = makeSession({ persistUserMessage, runAgentLoop });
361
+
362
+ const res = await sendMessage("start screen recording", session, {
363
+ commandIntent: { type: "start_recording", payload: "screen" },
364
+ });
365
+
366
+ expect(res.status).toBe(202);
367
+ expect(persistUserMessage).toHaveBeenCalledTimes(1);
368
+ expect(runAgentLoop).toHaveBeenCalledTimes(1);
369
+ });
370
+
352
371
  test("stop recording commands pass through to the agent loop", async () => {
353
372
  const persistUserMessage = mock(async () => "persisted-msg-id");
354
373
  const runAgentLoop = mock(async () => undefined);
@@ -38,7 +38,6 @@ mock.module("../config/loader.js", () => ({
38
38
  ui: {},
39
39
  model: "test",
40
40
  provider: "test",
41
- apiKeys: {},
42
41
  memory: { enabled: false },
43
42
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
44
43
  }),
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Tests for workspace file collection in the log export route handler.
3
+ *
4
+ * Validates that `POST /v1/export` includes workspace files with correct
5
+ * filtering: text files included, SQLite DB files dumped as SQL, excluded
6
+ * directories (embedding-models/, data/qdrant/) absent, binary files skipped.
7
+ */
8
+
9
+ import {
10
+ mkdirSync,
11
+ mkdtempSync,
12
+ realpathSync,
13
+ rmSync,
14
+ symlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterAll, describe, expect, mock, test } from "bun:test";
20
+
21
+ // Set up temp directories before mocking
22
+ const testDir = realpathSync(
23
+ mkdtempSync(join(tmpdir(), "log-export-workspace-test-")),
24
+ );
25
+ const testWorkspaceDir = join(testDir, "workspace");
26
+ const testDbDir = join(testDir, "db");
27
+ const testDbPath = join(testDbDir, "assistant.db");
28
+
29
+ mkdirSync(testWorkspaceDir, { recursive: true });
30
+ mkdirSync(testDbDir, { recursive: true });
31
+
32
+ mock.module("../util/platform.js", () => ({
33
+ getRootDir: () => testDir,
34
+ getDataDir: () => testDir,
35
+ getWorkspaceDir: () => testWorkspaceDir,
36
+ getWorkspaceConfigPath: () => join(testWorkspaceDir, "config.json"),
37
+ isMacOS: () => process.platform === "darwin",
38
+ isLinux: () => process.platform === "linux",
39
+ isWindows: () => process.platform === "win32",
40
+ getPidPath: () => join(testDir, "test.pid"),
41
+ getDbPath: () => testDbPath,
42
+ getLogPath: () => join(testDir, "test.log"),
43
+ ensureDataDir: () => {},
44
+ }));
45
+
46
+ mock.module("../util/logger.js", () => ({
47
+ getLogger: () =>
48
+ new Proxy({} as Record<string, unknown>, {
49
+ get: () => () => {},
50
+ }),
51
+ }));
52
+
53
+ // Mock getSecureKeyAsync to avoid keychain access during tests
54
+ mock.module("../util/secure-keys.js", () => ({
55
+ getSecureKeyAsync: async () => undefined,
56
+ }));
57
+
58
+ import { initializeDb, resetDb } from "../memory/db.js";
59
+ import { logExportRouteDefinitions } from "../runtime/routes/log-export-routes.js";
60
+
61
+ initializeDb();
62
+
63
+ afterAll(() => {
64
+ resetDb();
65
+ try {
66
+ rmSync(testDir, { recursive: true });
67
+ } catch {
68
+ /* best effort */
69
+ }
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Helpers
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const routes = logExportRouteDefinitions();
77
+ const exportRoute = routes.find((r) => r.endpoint === "export")!;
78
+
79
+ async function callExport(): Promise<Response> {
80
+ const req = new Request("http://localhost/v1/export", {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify({}),
84
+ });
85
+ const url = new URL(req.url);
86
+ return exportRoute.handler({
87
+ req,
88
+ url,
89
+ server: null as never,
90
+ authContext: {} as never,
91
+ params: {},
92
+ });
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Seed workspace files
97
+ // ---------------------------------------------------------------------------
98
+
99
+ // Text files — should be included
100
+ writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), "# My Identity\nHello");
101
+ mkdirSync(join(testWorkspaceDir, "notes"), { recursive: true });
102
+ writeFileSync(join(testWorkspaceDir, "notes", "daily.txt"), "Some daily notes");
103
+
104
+ // SQLite DB file — should be dumped as .sql
105
+ mkdirSync(join(testWorkspaceDir, "data", "db"), { recursive: true });
106
+ // Create a real sqlite db with a table
107
+ import { Database } from "bun:sqlite";
108
+ const wsDbPath = join(testWorkspaceDir, "data", "db", "assistant.db");
109
+ const wsDb = new Database(wsDbPath);
110
+ wsDb.run("CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)");
111
+ wsDb.run("INSERT INTO test_table (name) VALUES ('hello')");
112
+ wsDb.close();
113
+
114
+ // Excluded directory: embedding-models/
115
+ mkdirSync(join(testWorkspaceDir, "embedding-models"), { recursive: true });
116
+ writeFileSync(
117
+ join(testWorkspaceDir, "embedding-models", "model.bin"),
118
+ "large binary model data",
119
+ );
120
+
121
+ // Excluded directory: data/qdrant/
122
+ mkdirSync(join(testWorkspaceDir, "data", "qdrant"), { recursive: true });
123
+ writeFileSync(
124
+ join(testWorkspaceDir, "data", "qdrant", "index.bin"),
125
+ "vector index data",
126
+ );
127
+
128
+ // Binary file — should be skipped
129
+ writeFileSync(
130
+ join(testWorkspaceDir, "binary-file.dat"),
131
+ Buffer.from([0x48, 0x65, 0x6c, 0x00, 0x6f]), // contains null byte
132
+ );
133
+
134
+ // config.json at workspace root — should be skipped (already in configSnapshot)
135
+ writeFileSync(
136
+ join(testWorkspaceDir, "config.json"),
137
+ JSON.stringify({ provider: "anthropic" }),
138
+ );
139
+
140
+ // Symlink pointing outside workspace — should be skipped
141
+ const outsideFile = join(testDir, "outside-secret.txt");
142
+ writeFileSync(outsideFile, "sensitive data outside workspace");
143
+ try {
144
+ symlinkSync(outsideFile, join(testWorkspaceDir, "sneaky-link.txt"));
145
+ } catch {
146
+ // Symlink creation may fail on some platforms; tests will still pass
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Tests
151
+ // ---------------------------------------------------------------------------
152
+
153
+ describe("POST /v1/export — workspace files", () => {
154
+ test("includes text files in workspaceFiles", async () => {
155
+ const res = await callExport();
156
+ expect(res.status).toBe(200);
157
+ const body = (await res.json()) as {
158
+ workspaceFiles: Record<string, string>;
159
+ };
160
+ expect(body.workspaceFiles["IDENTITY.md"]).toBe("# My Identity\nHello");
161
+ expect(body.workspaceFiles["notes/daily.txt"]).toBe("Some daily notes");
162
+ });
163
+
164
+ test("dumps SQLite DB files as .sql text", async () => {
165
+ const res = await callExport();
166
+ const body = (await res.json()) as {
167
+ workspaceFiles: Record<string, string>;
168
+ };
169
+ const sqlKey = "data/db/assistant.db.sql";
170
+ expect(body.workspaceFiles[sqlKey]).toBeDefined();
171
+ expect(body.workspaceFiles[sqlKey]).toContain("CREATE TABLE");
172
+ expect(body.workspaceFiles[sqlKey]).toContain("test_table");
173
+ // The raw .db file should NOT be present
174
+ expect(body.workspaceFiles["data/db/assistant.db"]).toBeUndefined();
175
+ });
176
+
177
+ test("excludes embedding-models/ directory", async () => {
178
+ const res = await callExport();
179
+ const body = (await res.json()) as {
180
+ workspaceFiles: Record<string, string>;
181
+ };
182
+ const embeddingKeys = Object.keys(body.workspaceFiles).filter((k) =>
183
+ k.startsWith("embedding-models/"),
184
+ );
185
+ expect(embeddingKeys).toHaveLength(0);
186
+ });
187
+
188
+ test("excludes data/qdrant/ directory", async () => {
189
+ const res = await callExport();
190
+ const body = (await res.json()) as {
191
+ workspaceFiles: Record<string, string>;
192
+ };
193
+ const qdrantKeys = Object.keys(body.workspaceFiles).filter((k) =>
194
+ k.startsWith("data/qdrant/"),
195
+ );
196
+ expect(qdrantKeys).toHaveLength(0);
197
+ });
198
+
199
+ test("skips binary files containing null bytes", async () => {
200
+ const res = await callExport();
201
+ const body = (await res.json()) as {
202
+ workspaceFiles: Record<string, string>;
203
+ };
204
+ expect(body.workspaceFiles["binary-file.dat"]).toBeUndefined();
205
+ });
206
+
207
+ test("excludes config.json at workspace root", async () => {
208
+ const res = await callExport();
209
+ const body = (await res.json()) as {
210
+ workspaceFiles: Record<string, string>;
211
+ };
212
+ expect(body.workspaceFiles["config.json"]).toBeUndefined();
213
+ });
214
+
215
+ test("skips symlinks", async () => {
216
+ const res = await callExport();
217
+ const body = (await res.json()) as {
218
+ workspaceFiles: Record<string, string>;
219
+ };
220
+ expect(body.workspaceFiles["sneaky-link.txt"]).toBeUndefined();
221
+ });
222
+
223
+ test("response includes workspaceFiles field", async () => {
224
+ const res = await callExport();
225
+ const body = (await res.json()) as {
226
+ success: boolean;
227
+ workspaceFiles: Record<string, string>;
228
+ };
229
+ expect(body.success).toBe(true);
230
+ expect(body.workspaceFiles).toBeDefined();
231
+ expect(typeof body.workspaceFiles).toBe("object");
232
+ });
233
+ });
@@ -110,7 +110,7 @@ describe("buildManagedBaseUrl", () => {
110
110
  "https://platform.example.com/v1/runtime-proxy/openai",
111
111
  );
112
112
  expect(buildManagedBaseUrl("anthropic")).toBe(
113
- "https://platform.example.com/v1/runtime-proxy/vertex",
113
+ "https://platform.example.com/v1/runtime-proxy/anthropic",
114
114
  );
115
115
  expect(buildManagedBaseUrl("gemini")).toBe(
116
116
  "https://platform.example.com/v1/runtime-proxy/vertex",
@@ -9,7 +9,6 @@ let TEST_DIR = "";
9
9
  const mockConfig = {
10
10
  provider: "anthropic",
11
11
  model: "test",
12
- apiKeys: {},
13
12
  maxTokens: 4096,
14
13
  dataDir: "/tmp",
15
14
  timeouts: {
@@ -22,11 +22,16 @@ let lastGenerateCredentials: unknown = null;
22
22
  mock.module("../config/loader.js", () => ({
23
23
  getConfig: () => ({
24
24
  ui: {},
25
-
26
- apiKeys: { gemini: mockApiKey },
27
25
  }),
28
26
  }));
29
27
 
28
+ mock.module("../security/secure-keys.js", () => ({
29
+ getSecureKeyAsync: async (account: string) => {
30
+ if (account === "gemini") return mockApiKey;
31
+ return undefined;
32
+ },
33
+ }));
34
+
30
35
  mock.module("../media/gemini-image-service.js", () => ({
31
36
  generateImage: async (
32
37
  credentials: unknown,
@@ -52,7 +52,6 @@ mock.module("../config/loader.js", () => ({
52
52
 
53
53
  model: "test",
54
54
  provider: "test",
55
- apiKeys: {},
56
55
  memory: { enabled: false },
57
56
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
58
57
  timeouts: { shellDefaultTimeoutSec: 30, shellMaxTimeoutSec: 60 },
@@ -935,7 +935,6 @@ describe("Memory regressions", () => {
935
935
  const config = {
936
936
  ...DEFAULT_CONFIG,
937
937
  provider: "anthropic" as const,
938
- apiKeys: {},
939
938
  memory: {
940
939
  ...DEFAULT_CONFIG.memory,
941
940
  embeddings: {
@@ -82,7 +82,6 @@ mock.module("../config/loader.js", () => ({
82
82
  ui: {},
83
83
  model: "test",
84
84
  provider: "test",
85
- apiKeys: {},
86
85
  memory: { enabled: false },
87
86
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
88
87
  secretDetection: { enabled: false },
@@ -56,7 +56,6 @@ mock.module("../config/loader.js", () => ({
56
56
  ui: {},
57
57
  model: "test",
58
58
  provider: "test",
59
- apiKeys: {},
60
59
  memory: { enabled: false },
61
60
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
62
61
  secretDetection: { enabled: false },
@@ -77,7 +77,6 @@ mock.module("../config/loader.js", () => ({
77
77
  ui: {},
78
78
  model: "test",
79
79
  provider: "test",
80
- apiKeys: {},
81
80
  memory: { enabled: false },
82
81
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
83
82
  secretDetection: { enabled: false },
@@ -63,7 +63,6 @@ mock.module("../config/loader.js", () => ({
63
63
  ui: {},
64
64
  model: "test",
65
65
  provider: "test",
66
- apiKeys: {},
67
66
  memory: { enabled: false },
68
67
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
69
68
  secretDetection: { enabled: false },
@@ -52,7 +52,6 @@ mock.module("../config/loader.js", () => ({
52
52
  ui: {},
53
53
  model: "test",
54
54
  provider: "test",
55
- apiKeys: {},
56
55
  memory: { enabled: false },
57
56
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
58
57
  secretDetection: { enabled: false },