@vellumai/assistant 0.5.9 → 0.5.11

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 (278) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +48 -48
  3. package/Dockerfile +2 -0
  4. package/README.md +1 -1
  5. package/docs/architecture/integrations.md +6 -13
  6. package/docs/architecture/memory.md +7 -12
  7. package/docs/architecture/security.md +5 -5
  8. package/docs/credential-execution-service.md +9 -9
  9. package/docs/skills.md +1 -1
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7130 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/channel-approvals.test.ts +7 -7
  23. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  24. package/src/__tests__/config-schema.test.ts +10 -2
  25. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  26. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  27. package/src/__tests__/conversation-title-service.test.ts +2 -15
  28. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  29. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  30. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  31. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  32. package/src/__tests__/credentials-cli.test.ts +3 -3
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  34. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  35. package/src/__tests__/heartbeat-service.test.ts +35 -0
  36. package/src/__tests__/host-shell-tool.test.ts +1 -1
  37. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  38. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  39. package/src/__tests__/log-export-workspace.test.ts +1 -1
  40. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  42. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  43. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  44. package/src/__tests__/memory-regressions.test.ts +53 -42
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  46. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  47. package/src/__tests__/oauth-cli.test.ts +98 -551
  48. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  49. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  50. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  51. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  52. package/src/__tests__/secret-ingress.test.ts +283 -0
  53. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  54. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  55. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  56. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  57. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  58. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  59. package/src/__tests__/skill-memory.test.ts +2 -4
  60. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  61. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  62. package/src/__tests__/skills.test.ts +16 -2
  63. package/src/__tests__/slack-channel-config.test.ts +1 -1
  64. package/src/__tests__/slack-skill.test.ts +5 -69
  65. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  66. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
  67. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
  68. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  69. package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
  70. package/src/acp/client-handler.ts +113 -31
  71. package/src/acp/session-manager.ts +29 -27
  72. package/src/approvals/guardian-request-resolvers.ts +1 -1
  73. package/src/cli/AGENTS.md +73 -0
  74. package/src/cli/commands/autonomy.ts +3 -5
  75. package/src/cli/commands/credential-execution.ts +1 -2
  76. package/src/cli/commands/credentials.ts +4 -4
  77. package/src/cli/commands/memory.ts +2 -3
  78. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  79. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  80. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  81. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  82. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  83. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  84. package/src/cli/commands/oauth/apps.ts +29 -11
  85. package/src/cli/commands/oauth/connect.ts +373 -0
  86. package/src/cli/commands/oauth/connections.ts +14 -493
  87. package/src/cli/commands/oauth/disconnect.ts +333 -0
  88. package/src/cli/commands/oauth/index.ts +62 -10
  89. package/src/cli/commands/oauth/mode.ts +263 -0
  90. package/src/cli/commands/oauth/ping.ts +222 -0
  91. package/src/cli/commands/oauth/providers.ts +30 -3
  92. package/src/cli/commands/oauth/request.ts +576 -0
  93. package/src/cli/commands/oauth/shared.ts +132 -0
  94. package/src/cli/commands/oauth/status.ts +202 -0
  95. package/src/cli/commands/oauth/token.ts +159 -0
  96. package/src/cli/commands/platform.ts +20 -14
  97. package/src/cli.ts +82 -17
  98. package/src/config/assistant-feature-flags.ts +74 -11
  99. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  100. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  101. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  102. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  103. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  104. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  105. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  106. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  107. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  108. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  109. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  111. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  112. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  113. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  114. package/src/config/bundled-tool-registry.ts +2 -19
  115. package/src/config/env.ts +5 -1
  116. package/src/config/feature-flag-registry.json +57 -41
  117. package/src/config/loader.ts +4 -0
  118. package/src/config/schemas/platform.ts +0 -8
  119. package/src/config/schemas/security.ts +9 -1
  120. package/src/config/schemas/services.ts +1 -1
  121. package/src/config/skill-state.ts +1 -3
  122. package/src/config/skills.ts +2 -4
  123. package/src/credential-execution/feature-gates.ts +9 -16
  124. package/src/credential-execution/process-manager.ts +12 -0
  125. package/src/daemon/config-watcher.ts +4 -0
  126. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  127. package/src/daemon/conversation-agent-loop.ts +49 -2
  128. package/src/daemon/conversation-memory.ts +0 -1
  129. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  130. package/src/daemon/handlers/conversations.ts +41 -33
  131. package/src/daemon/lifecycle.ts +28 -5
  132. package/src/daemon/message-types/acp.ts +0 -15
  133. package/src/daemon/message-types/memory.ts +0 -1
  134. package/src/daemon/message-types/messages.ts +9 -1
  135. package/src/daemon/message-types/schedules.ts +9 -0
  136. package/src/daemon/server.ts +19 -7
  137. package/src/email/feature-gate.ts +3 -3
  138. package/src/heartbeat/heartbeat-service.ts +48 -0
  139. package/src/inbound/platform-callback-registration.ts +61 -7
  140. package/src/mcp/mcp-oauth-provider.ts +3 -3
  141. package/src/memory/app-store.ts +3 -3
  142. package/src/memory/conversation-crud.ts +124 -0
  143. package/src/memory/conversation-title-service.ts +7 -17
  144. package/src/memory/db-init.ts +8 -0
  145. package/src/memory/embedding-local.ts +47 -2
  146. package/src/memory/indexer.ts +13 -10
  147. package/src/memory/items-extractor.ts +12 -4
  148. package/src/memory/job-utils.ts +5 -0
  149. package/src/memory/jobs-store.ts +10 -2
  150. package/src/memory/journal-memory.ts +6 -2
  151. package/src/memory/llm-request-log-store.ts +88 -21
  152. package/src/memory/memory-recall-log-store.ts +128 -0
  153. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  154. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  155. package/src/memory/migrations/index.ts +2 -0
  156. package/src/memory/migrations/validate-migration-state.ts +14 -1
  157. package/src/memory/retriever.test.ts +4 -5
  158. package/src/memory/schema/infrastructure.ts +31 -0
  159. package/src/memory/schema/oauth.ts +3 -0
  160. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  161. package/src/oauth/connect-orchestrator.ts +54 -0
  162. package/src/oauth/manual-token-connection.ts +5 -5
  163. package/src/oauth/oauth-store.ts +26 -5
  164. package/src/oauth/seed-providers.ts +10 -1
  165. package/src/permissions/checker.ts +2 -2
  166. package/src/permissions/trust-client.ts +2 -2
  167. package/src/platform/client.ts +2 -2
  168. package/src/prompts/journal-context.ts +6 -1
  169. package/src/providers/anthropic/client.ts +143 -1
  170. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  171. package/src/runtime/auth/route-policy.ts +0 -1
  172. package/src/runtime/btw-sidechain.ts +7 -1
  173. package/src/runtime/channel-approvals.ts +2 -2
  174. package/src/runtime/channel-readiness-service.ts +30 -7
  175. package/src/runtime/http-router.ts +31 -0
  176. package/src/runtime/http-server.ts +21 -4
  177. package/src/runtime/http-types.ts +2 -0
  178. package/src/runtime/pending-interactions.ts +21 -3
  179. package/src/runtime/routes/acp-routes.ts +46 -28
  180. package/src/runtime/routes/app-management-routes.ts +123 -0
  181. package/src/runtime/routes/app-routes.ts +31 -0
  182. package/src/runtime/routes/approval-routes.ts +108 -3
  183. package/src/runtime/routes/attachment-routes.ts +45 -0
  184. package/src/runtime/routes/avatar-routes.ts +16 -0
  185. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  186. package/src/runtime/routes/btw-routes.ts +20 -0
  187. package/src/runtime/routes/call-routes.ts +81 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  189. package/src/runtime/routes/channel-routes.ts +18 -0
  190. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  191. package/src/runtime/routes/contact-routes.ts +77 -0
  192. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  193. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  194. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  195. package/src/runtime/routes/conversation-routes.ts +115 -38
  196. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  197. package/src/runtime/routes/debug-routes.ts +23 -0
  198. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  199. package/src/runtime/routes/documents-routes.ts +42 -0
  200. package/src/runtime/routes/events-routes.ts +10 -0
  201. package/src/runtime/routes/global-search-routes.ts +35 -0
  202. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  203. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  204. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  205. package/src/runtime/routes/host-bash-routes.ts +16 -1
  206. package/src/runtime/routes/host-cu-routes.ts +23 -1
  207. package/src/runtime/routes/host-file-routes.ts +18 -1
  208. package/src/runtime/routes/identity-routes.ts +35 -0
  209. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  210. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  211. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  212. package/src/runtime/routes/integrations/twilio.ts +32 -22
  213. package/src/runtime/routes/invite-routes.ts +83 -0
  214. package/src/runtime/routes/log-export-routes.ts +14 -0
  215. package/src/runtime/routes/memory-item-routes.ts +99 -1
  216. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  217. package/src/runtime/routes/migration-routes.ts +40 -0
  218. package/src/runtime/routes/notification-routes.ts +20 -0
  219. package/src/runtime/routes/oauth-apps.ts +11 -3
  220. package/src/runtime/routes/pairing-routes.ts +15 -0
  221. package/src/runtime/routes/recording-routes.ts +72 -0
  222. package/src/runtime/routes/schedule-routes.ts +77 -5
  223. package/src/runtime/routes/secret-routes.ts +63 -1
  224. package/src/runtime/routes/settings-routes.ts +91 -1
  225. package/src/runtime/routes/skills-routes.ts +98 -16
  226. package/src/runtime/routes/subagents-routes.ts +38 -3
  227. package/src/runtime/routes/surface-action-routes.ts +66 -24
  228. package/src/runtime/routes/surface-content-routes.ts +20 -0
  229. package/src/runtime/routes/telemetry-routes.ts +12 -0
  230. package/src/runtime/routes/trace-event-routes.ts +25 -0
  231. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  232. package/src/runtime/routes/tts-routes.ts +15 -4
  233. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  234. package/src/runtime/routes/usage-routes.ts +59 -0
  235. package/src/runtime/routes/watch-routes.ts +28 -0
  236. package/src/runtime/routes/work-items-routes.ts +59 -0
  237. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  238. package/src/runtime/routes/workspace-routes.ts +102 -0
  239. package/src/schedule/scheduler.ts +7 -1
  240. package/src/security/AGENTS.md +7 -0
  241. package/src/security/credential-backend.ts +1 -1
  242. package/src/security/encrypted-store.ts +3 -3
  243. package/src/security/oauth2.ts +55 -0
  244. package/src/security/secret-ingress.ts +174 -0
  245. package/src/security/secret-patterns.ts +133 -0
  246. package/src/security/secret-scanner.ts +28 -117
  247. package/src/signals/confirm.ts +12 -8
  248. package/src/signals/user-message.ts +18 -3
  249. package/src/skills/skill-memory.ts +1 -2
  250. package/src/tasks/task-runner.ts +7 -1
  251. package/src/tools/credentials/broker.ts +1 -1
  252. package/src/tools/credentials/metadata-store.ts +1 -1
  253. package/src/tools/credentials/vault.ts +2 -3
  254. package/src/tools/memory/definitions.ts +1 -1
  255. package/src/tools/memory/handlers.test.ts +2 -4
  256. package/src/tools/skills/load.ts +1 -1
  257. package/src/tools/terminal/safe-env.ts +7 -0
  258. package/src/tools/tool-manifest.ts +1 -1
  259. package/src/util/log-redact.ts +9 -34
  260. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
  261. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
  262. package/src/workspace/migrations/AGENTS.md +11 -0
  263. package/src/workspace/migrations/runner.ts +16 -6
  264. package/src/workspace/migrations/types.ts +7 -0
  265. package/docs/architecture/keychain-broker.md +0 -69
  266. package/src/__tests__/keychain-broker-client.test.ts +0 -800
  267. package/src/cli/commands/oauth/platform.ts +0 -525
  268. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  269. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  270. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  271. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  272. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  273. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  274. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  275. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  276. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  277. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  278. package/src/security/keychain-broker-client.ts +0 -446
@@ -0,0 +1,119 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { credentialKey } from "../security/credential-key.js";
4
+
5
+ let mockIsContainerized = true;
6
+ let mockPlatformBaseUrl = "";
7
+ let mockPlatformAssistantId = "";
8
+ let mockPlatformInternalApiKey = "";
9
+ let mockSecureKeys: Record<string, string> = {};
10
+
11
+ mock.module("../config/env-registry.js", () => ({
12
+ getIsContainerized: () => mockIsContainerized,
13
+ }));
14
+
15
+ mock.module("../config/env.js", () => ({
16
+ getPlatformBaseUrl: () => mockPlatformBaseUrl,
17
+ getPlatformAssistantId: () => mockPlatformAssistantId,
18
+ getPlatformInternalApiKey: () => mockPlatformInternalApiKey,
19
+ }));
20
+
21
+ mock.module("../security/secure-keys.js", () => ({
22
+ getSecureKeyAsync: async (key: string) => mockSecureKeys[key] ?? undefined,
23
+ }));
24
+
25
+ mock.module("../util/logger.js", () => ({
26
+ getLogger: () => ({
27
+ debug: () => {},
28
+ info: () => {},
29
+ warn: () => {},
30
+ error: () => {},
31
+ }),
32
+ }));
33
+
34
+ const originalFetch = globalThis.fetch;
35
+
36
+ const {
37
+ registerCallbackRoute,
38
+ resolvePlatformCallbackRegistrationContext,
39
+ } = await import("../inbound/platform-callback-registration.js");
40
+
41
+ describe("platform callback registration", () => {
42
+ beforeEach(() => {
43
+ mockIsContainerized = true;
44
+ mockPlatformBaseUrl = "";
45
+ mockPlatformAssistantId = "";
46
+ mockPlatformInternalApiKey = "";
47
+ mockSecureKeys = {};
48
+ globalThis.fetch = originalFetch;
49
+ });
50
+
51
+ afterEach(() => {
52
+ globalThis.fetch = originalFetch;
53
+ });
54
+
55
+ test("resolves managed callback context from stored credentials", async () => {
56
+ mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
57
+ "https://platform.example.com";
58
+ mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
59
+ "11111111-2222-4333-8444-555555555555";
60
+ mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
61
+ "ast-managed-key";
62
+
63
+ const context = await resolvePlatformCallbackRegistrationContext();
64
+
65
+ expect(context.enabled).toBe(true);
66
+ expect(context.containerized).toBe(true);
67
+ expect(context.platformBaseUrl).toBe("https://platform.example.com");
68
+ expect(context.assistantId).toBe(
69
+ "11111111-2222-4333-8444-555555555555",
70
+ );
71
+ expect(context.hasInternalApiKey).toBe(false);
72
+ expect(context.hasAssistantApiKey).toBe(true);
73
+ expect(context.authHeader).toBe("Api-Key ast-managed-key");
74
+ });
75
+
76
+ test("registerCallbackRoute falls back to assistant API key auth", async () => {
77
+ mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
78
+ "https://platform.example.com";
79
+ mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
80
+ "11111111-2222-4333-8444-555555555555";
81
+ mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
82
+ "ast-managed-key";
83
+
84
+ globalThis.fetch = mock(
85
+ async (input: RequestInfo | URL, init?: RequestInit) => {
86
+ expect(String(input)).toBe(
87
+ "https://platform.example.com/v1/internal/gateway/callback-routes/register/",
88
+ );
89
+ const headers = new Headers(init?.headers);
90
+ expect(headers.get("Authorization")).toBe("Api-Key ast-managed-key");
91
+ expect(headers.get("Content-Type")).toBe("application/json");
92
+ expect(JSON.parse(String(init?.body))).toEqual({
93
+ assistant_id: "11111111-2222-4333-8444-555555555555",
94
+ callback_path: "webhooks/telegram",
95
+ type: "telegram",
96
+ });
97
+
98
+ return new Response(
99
+ JSON.stringify({
100
+ callback_url:
101
+ "https://platform.example.com/v1/gateway/callbacks/x/",
102
+ callback_path:
103
+ "11111111-2222-4333-8444-555555555555/webhooks/telegram",
104
+ type: "telegram",
105
+ assistant_id: "11111111-2222-4333-8444-555555555555",
106
+ }),
107
+ {
108
+ status: 201,
109
+ headers: { "content-type": "application/json" },
110
+ },
111
+ );
112
+ },
113
+ ) as unknown as typeof fetch;
114
+
115
+ await expect(
116
+ registerCallbackRoute("webhooks/telegram", "telegram"),
117
+ ).resolves.toBe("https://platform.example.com/v1/gateway/callbacks/x/");
118
+ });
119
+ });
@@ -0,0 +1,261 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mocks — must be declared before any imports that depend on them
5
+ // ---------------------------------------------------------------------------
6
+
7
+ let mockConfig: Record<string, unknown> = {
8
+ secretDetection: {
9
+ enabled: true,
10
+ blockIngress: true,
11
+ },
12
+ };
13
+
14
+ mock.module("../config/loader.js", () => ({
15
+ getConfig: () => mockConfig,
16
+ loadConfig: () => mockConfig,
17
+ invalidateConfigCache: () => {},
18
+ }));
19
+
20
+ mock.module("../util/logger.js", () => ({
21
+ getLogger: () =>
22
+ new Proxy({} as Record<string, unknown>, {
23
+ get: () => () => {},
24
+ }),
25
+ }));
26
+
27
+ mock.module("../util/platform.js", () => ({
28
+ getRootDir: () => "/tmp/vellum-test-secret-ingress-channel",
29
+ getWorkspaceDir: () => "/tmp/vellum-test-secret-ingress-channel/workspace",
30
+ }));
31
+
32
+ const storePayloadMock = mock((_eventId: string, _payload: unknown) => {});
33
+ const clearPayloadMock = mock((_eventId: string) => {});
34
+
35
+ mock.module("../memory/delivery-crud.js", () => ({
36
+ storePayload: (eventId: string, payload: unknown) =>
37
+ storePayloadMock(eventId, payload),
38
+ clearPayload: (eventId: string) => clearPayloadMock(eventId),
39
+ recordInbound: () => ({
40
+ eventId: "evt-test",
41
+ conversationId: "conv-test",
42
+ accepted: true,
43
+ duplicate: false,
44
+ }),
45
+ }));
46
+
47
+ const markProcessedMock = mock((_eventId: string) => {});
48
+
49
+ mock.module("../memory/delivery-status.js", () => ({
50
+ markProcessed: (eventId: string) => markProcessedMock(eventId),
51
+ }));
52
+
53
+ mock.module("../memory/conversation-attention-store.js", () => ({
54
+ recordConversationSeenSignal: () => {},
55
+ }));
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Imports (after mocks)
59
+ // ---------------------------------------------------------------------------
60
+
61
+ import {
62
+ runSecretIngressCheck,
63
+ type SecretIngressCheckParams,
64
+ } from "../runtime/routes/inbound-stages/secret-ingress-check.js";
65
+ import { resetAllowlist } from "../security/secret-allowlist.js";
66
+
67
+ function makeParams(
68
+ overrides: Partial<SecretIngressCheckParams> = {},
69
+ ): SecretIngressCheckParams {
70
+ return {
71
+ eventId: "evt-test-1",
72
+ sourceChannel: "slack",
73
+ conversationExternalId: "ext-conv-1",
74
+ externalMessageId: "ext-msg-1",
75
+ conversationId: "conv-1",
76
+ content: "hello",
77
+ trimmedContent: "hello",
78
+ attachmentIds: undefined,
79
+ sourceMetadata: undefined,
80
+ actorDisplayName: "Test User",
81
+ actorExternalId: "user-1",
82
+ actorUsername: "testuser",
83
+ trustCtx: {
84
+ trustClass: "member" as const,
85
+ sourceChannel: "slack" as const,
86
+ } as any,
87
+ replyCallbackUrl: undefined,
88
+ canonicalAssistantId: "self",
89
+ ...overrides,
90
+ };
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Tests
95
+ // ---------------------------------------------------------------------------
96
+
97
+ describe("secret ingress — channel inbound path", () => {
98
+ beforeEach(() => {
99
+ mockConfig = {
100
+ secretDetection: {
101
+ enabled: true,
102
+ blockIngress: true,
103
+ },
104
+ };
105
+ storePayloadMock.mockClear();
106
+ clearPayloadMock.mockClear();
107
+ markProcessedMock.mockClear();
108
+ resetAllowlist();
109
+ });
110
+
111
+ test("channel inbound with GOCSPX- secret returns blocked: true", () => {
112
+ const secret = "GOCSPX-abcdefghijklmnopqrstuvwxyz12";
113
+ const result = runSecretIngressCheck(
114
+ makeParams({
115
+ content: `My secret is ${secret}`,
116
+ trimmedContent: `My secret is ${secret}`,
117
+ }),
118
+ );
119
+
120
+ expect(result.blocked).toBe(true);
121
+ expect(result.detectedTypes).toContain("Google OAuth Client Secret");
122
+ });
123
+
124
+ test("channel inbound with normal text returns blocked: false", () => {
125
+ const result = runSecretIngressCheck(
126
+ makeParams({
127
+ content: "Hello, how can I help?",
128
+ trimmedContent: "Hello, how can I help?",
129
+ }),
130
+ );
131
+
132
+ expect(result.blocked).toBe(false);
133
+ });
134
+
135
+ test("payload is cleared when blocked", () => {
136
+ const secret = "GOCSPX-abcdefghijklmnopqrstuvwxyz12";
137
+ runSecretIngressCheck(
138
+ makeParams({
139
+ eventId: "evt-clear-test",
140
+ content: `Secret: ${secret}`,
141
+ trimmedContent: `Secret: ${secret}`,
142
+ }),
143
+ );
144
+
145
+ // storePayload should have been called (it persists before checking)
146
+ expect(storePayloadMock).toHaveBeenCalledTimes(1);
147
+ // clearPayload should have been called to remove the secret-bearing payload
148
+ expect(clearPayloadMock).toHaveBeenCalledWith("evt-clear-test");
149
+ });
150
+
151
+ test("payload is NOT cleared for normal messages", () => {
152
+ runSecretIngressCheck(
153
+ makeParams({
154
+ content: "Normal message",
155
+ trimmedContent: "Normal message",
156
+ }),
157
+ );
158
+
159
+ expect(storePayloadMock).toHaveBeenCalledTimes(1);
160
+ expect(clearPayloadMock).not.toHaveBeenCalled();
161
+ });
162
+
163
+ test("event is marked as processed (not dead-lettered) when blocked — verified via caller contract", () => {
164
+ // The inbound-message-handler calls markProcessed when runSecretIngressCheck
165
+ // returns blocked: true. We verify the check returns blocked: true, which
166
+ // triggers the markProcessed call in the handler.
167
+ const secret = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234";
168
+ const result = runSecretIngressCheck(
169
+ makeParams({
170
+ content: secret,
171
+ trimmedContent: secret,
172
+ }),
173
+ );
174
+
175
+ expect(result.blocked).toBe(true);
176
+
177
+ // Simulate the handler's behavior: mark processed on block
178
+ // (This mirrors inbound-message-handler.ts lines 663-666)
179
+ if (result.blocked) {
180
+ markProcessedMock("evt-test-1");
181
+ }
182
+ expect(markProcessedMock).toHaveBeenCalledWith("evt-test-1");
183
+ });
184
+
185
+ test("channel inbound with GitHub token is blocked", () => {
186
+ const token = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234";
187
+ const result = runSecretIngressCheck(
188
+ makeParams({
189
+ content: `Token: ${token}`,
190
+ trimmedContent: `Token: ${token}`,
191
+ }),
192
+ );
193
+
194
+ expect(result.blocked).toBe(true);
195
+ expect(result.detectedTypes).toContain("GitHub Token");
196
+ });
197
+
198
+ test("channel inbound with Slack bot token is blocked", () => {
199
+ const token = "xoxb-1234567890-9876543210-AbCdEfGhIjKlMnOpQrStUvWx";
200
+ const result = runSecretIngressCheck(
201
+ makeParams({
202
+ content: token,
203
+ trimmedContent: token,
204
+ }),
205
+ );
206
+
207
+ expect(result.blocked).toBe(true);
208
+ expect(result.detectedTypes).toContain("Slack Bot Token");
209
+ });
210
+
211
+ test("channel inbound with JWT is not blocked (excluded pattern)", () => {
212
+ const jwt =
213
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
214
+ const result = runSecretIngressCheck(
215
+ makeParams({
216
+ content: jwt,
217
+ trimmedContent: jwt,
218
+ }),
219
+ );
220
+
221
+ expect(result.blocked).toBe(false);
222
+ });
223
+
224
+ test("channel inbound with blockIngress: false allows secrets through", () => {
225
+ mockConfig = {
226
+ secretDetection: {
227
+ enabled: true,
228
+ blockIngress: false,
229
+ },
230
+ };
231
+
232
+ const secret = "GOCSPX-abcdefghijklmnopqrstuvwxyz12";
233
+ const result = runSecretIngressCheck(
234
+ makeParams({
235
+ content: secret,
236
+ trimmedContent: secret,
237
+ }),
238
+ );
239
+
240
+ expect(result.blocked).toBe(false);
241
+ expect(clearPayloadMock).not.toHaveBeenCalled();
242
+ });
243
+
244
+ test("background dispatch is skipped when blocked (caller reads blocked flag)", () => {
245
+ // The inbound-message-handler skips processChannelMessageInBackground
246
+ // when ingressResult.blocked is true. We verify the function returns
247
+ // the correct blocked flag that the handler uses for this decision.
248
+ const secret = "AKIAIOSFODNN7EXAMPLE";
249
+ const result = runSecretIngressCheck(
250
+ makeParams({
251
+ content: `AWS: ${secret}`,
252
+ trimmedContent: `AWS: ${secret}`,
253
+ }),
254
+ );
255
+
256
+ // blocked: true means the caller (inbound-message-handler) will skip
257
+ // the background dispatch branch
258
+ expect(result.blocked).toBe(true);
259
+ expect(result.detectedTypes).toContain("AWS Access Key");
260
+ });
261
+ });
@@ -0,0 +1,201 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mocks — must be declared before any imports that depend on them
5
+ // ---------------------------------------------------------------------------
6
+
7
+ let mockConfig: Record<string, unknown> = {
8
+ secretDetection: {
9
+ enabled: true,
10
+ blockIngress: true,
11
+ },
12
+ };
13
+
14
+ mock.module("../config/loader.js", () => ({
15
+ getConfig: () => mockConfig,
16
+ loadConfig: () => mockConfig,
17
+ invalidateConfigCache: () => {},
18
+ }));
19
+
20
+ mock.module("../util/logger.js", () => ({
21
+ getLogger: () =>
22
+ new Proxy({} as Record<string, unknown>, {
23
+ get: () => () => {},
24
+ }),
25
+ }));
26
+
27
+ mock.module("../util/platform.js", () => ({
28
+ getRootDir: () => "/tmp/vellum-test-secret-ingress-cli",
29
+ getWorkspaceDir: () => "/tmp/vellum-test-secret-ingress-cli/workspace",
30
+ getSignalsDir: () => "/tmp/vellum-test-secret-ingress-cli/signals",
31
+ }));
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Test: CLI signal path uses registerUserMessageCallback which calls
35
+ // checkIngressForSecrets before calling persistAndProcessMessage.
36
+ //
37
+ // We test the callback behavior directly rather than the full signal file
38
+ // flow, since the signal handler is just file I/O around the callback.
39
+ // ---------------------------------------------------------------------------
40
+
41
+ import { resetAllowlist } from "../security/secret-allowlist.js";
42
+ import { checkIngressForSecrets } from "../security/secret-ingress.js";
43
+
44
+ /**
45
+ * Simulates the user-message callback registered in DaemonServer.start().
46
+ * This mirrors the logic in assistant/src/daemon/server.ts lines 512-556.
47
+ */
48
+ function makeUserMessageCallback() {
49
+ const persistAndProcessMessageMock = mock(
50
+ async (
51
+ _conversationKey: string,
52
+ _content: string,
53
+ _sourceChannel: string,
54
+ _sourceInterface: string,
55
+ ) => undefined,
56
+ );
57
+
58
+ const callback = async (params: {
59
+ conversationKey: string;
60
+ content: string;
61
+ sourceChannel: string;
62
+ sourceInterface: string;
63
+ }): Promise<{ accepted: boolean; error?: string; message?: string }> => {
64
+ // This is the secret check that runs before any persistence
65
+ const ingressResult = checkIngressForSecrets(params.content);
66
+ if (ingressResult.blocked) {
67
+ return {
68
+ accepted: false,
69
+ error: "secret_blocked" as const,
70
+ message: ingressResult.userNotice,
71
+ };
72
+ }
73
+
74
+ // If not blocked, would call persistAndProcessMessage
75
+ await persistAndProcessMessageMock(
76
+ params.conversationKey,
77
+ params.content,
78
+ params.sourceChannel,
79
+ params.sourceInterface,
80
+ );
81
+ return { accepted: true };
82
+ };
83
+
84
+ return { callback, persistAndProcessMessageMock };
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Tests
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe("secret ingress — CLI signal path", () => {
92
+ beforeEach(() => {
93
+ mockConfig = {
94
+ secretDetection: {
95
+ enabled: true,
96
+ blockIngress: true,
97
+ },
98
+ };
99
+ resetAllowlist();
100
+ });
101
+
102
+ test("CLI signal with secret content returns accepted: false with error: secret_blocked", async () => {
103
+ const { callback, persistAndProcessMessageMock } =
104
+ makeUserMessageCallback();
105
+
106
+ const result = await callback({
107
+ conversationKey: "test-key",
108
+ content: "Here is my token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234",
109
+ sourceChannel: "vellum",
110
+ sourceInterface: "cli",
111
+ });
112
+
113
+ expect(result.accepted).toBe(false);
114
+ expect(result.error).toBe("secret_blocked");
115
+ expect(result.message).toBeDefined();
116
+ expect(persistAndProcessMessageMock).not.toHaveBeenCalled();
117
+ });
118
+
119
+ test("CLI signal with normal text returns accepted: true", async () => {
120
+ const { callback, persistAndProcessMessageMock } =
121
+ makeUserMessageCallback();
122
+
123
+ const result = await callback({
124
+ conversationKey: "test-key",
125
+ content: "Hello, how are you?",
126
+ sourceChannel: "vellum",
127
+ sourceInterface: "cli",
128
+ });
129
+
130
+ expect(result.accepted).toBe(true);
131
+ expect(result.error).toBeUndefined();
132
+ expect(persistAndProcessMessageMock).toHaveBeenCalledTimes(1);
133
+ });
134
+
135
+ test("persistAndProcessMessage is NOT called when blocked", async () => {
136
+ const { callback, persistAndProcessMessageMock } =
137
+ makeUserMessageCallback();
138
+
139
+ // AWS access key
140
+ await callback({
141
+ conversationKey: "test-key",
142
+ content: "AWS key: AKIAIOSFODNN7EXAMPLE",
143
+ sourceChannel: "vellum",
144
+ sourceInterface: "cli",
145
+ });
146
+
147
+ expect(persistAndProcessMessageMock).not.toHaveBeenCalled();
148
+ });
149
+
150
+ test("CLI signal with Anthropic API key is blocked", async () => {
151
+ const { callback } = makeUserMessageCallback();
152
+
153
+ const key =
154
+ "sk-ant-api03-abcDefGhiJklMnoPqrStuVwxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIj";
155
+ const result = await callback({
156
+ conversationKey: "test-key",
157
+ content: `Key: ${key}`,
158
+ sourceChannel: "vellum",
159
+ sourceInterface: "cli",
160
+ });
161
+
162
+ expect(result.accepted).toBe(false);
163
+ expect(result.error).toBe("secret_blocked");
164
+ });
165
+
166
+ test("CLI signal with JWT is not blocked (excluded pattern)", async () => {
167
+ const { callback } = makeUserMessageCallback();
168
+
169
+ const result = await callback({
170
+ conversationKey: "test-key",
171
+ content:
172
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
173
+ sourceChannel: "vellum",
174
+ sourceInterface: "cli",
175
+ });
176
+
177
+ expect(result.accepted).toBe(true);
178
+ });
179
+
180
+ test("CLI signal with blockIngress: false allows secrets through", async () => {
181
+ mockConfig = {
182
+ secretDetection: {
183
+ enabled: true,
184
+ blockIngress: false,
185
+ },
186
+ };
187
+
188
+ const { callback, persistAndProcessMessageMock } =
189
+ makeUserMessageCallback();
190
+
191
+ const result = await callback({
192
+ conversationKey: "test-key",
193
+ content: "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234",
194
+ sourceChannel: "vellum",
195
+ sourceInterface: "cli",
196
+ });
197
+
198
+ expect(result.accepted).toBe(true);
199
+ expect(persistAndProcessMessageMock).toHaveBeenCalledTimes(1);
200
+ });
201
+ });