@vellumai/assistant 0.5.10 → 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 (263) hide show
  1. package/AGENTS.md +8 -0
  2. package/ARCHITECTURE.md +43 -43
  3. package/Dockerfile +2 -0
  4. package/docs/architecture/integrations.md +3 -10
  5. package/docs/architecture/memory.md +7 -12
  6. package/docs/credential-execution-service.md +9 -9
  7. package/docs/skills.md +1 -1
  8. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  9. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  10. package/openapi.yaml +7130 -0
  11. package/package.json +2 -1
  12. package/scripts/generate-openapi.ts +562 -0
  13. package/src/__tests__/acp-session.test.ts +239 -44
  14. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  17. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  18. package/src/__tests__/btw-routes.test.ts +8 -0
  19. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  20. package/src/__tests__/channel-approvals.test.ts +7 -7
  21. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  22. package/src/__tests__/config-schema.test.ts +10 -2
  23. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  24. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  25. package/src/__tests__/conversation-title-service.test.ts +2 -15
  26. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  27. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  28. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  29. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  30. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  31. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  32. package/src/__tests__/heartbeat-service.test.ts +35 -0
  33. package/src/__tests__/host-shell-tool.test.ts +1 -1
  34. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  35. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  36. package/src/__tests__/log-export-workspace.test.ts +1 -1
  37. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  38. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  39. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  40. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  41. package/src/__tests__/memory-regressions.test.ts +53 -42
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  43. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  44. package/src/__tests__/oauth-cli.test.ts +98 -551
  45. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  46. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  47. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  48. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  49. package/src/__tests__/secret-ingress.test.ts +283 -0
  50. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  51. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  52. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  53. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  54. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  55. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  56. package/src/__tests__/skill-memory.test.ts +2 -4
  57. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  58. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  59. package/src/__tests__/skills.test.ts +16 -2
  60. package/src/__tests__/slack-channel-config.test.ts +1 -1
  61. package/src/__tests__/slack-skill.test.ts +5 -69
  62. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  63. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  64. package/src/acp/client-handler.ts +113 -31
  65. package/src/acp/session-manager.ts +29 -27
  66. package/src/approvals/guardian-request-resolvers.ts +1 -1
  67. package/src/cli/AGENTS.md +73 -0
  68. package/src/cli/commands/autonomy.ts +3 -5
  69. package/src/cli/commands/credential-execution.ts +1 -2
  70. package/src/cli/commands/memory.ts +2 -3
  71. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  72. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  73. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  74. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  75. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  76. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  77. package/src/cli/commands/oauth/apps.ts +26 -8
  78. package/src/cli/commands/oauth/connect.ts +373 -0
  79. package/src/cli/commands/oauth/connections.ts +14 -493
  80. package/src/cli/commands/oauth/disconnect.ts +333 -0
  81. package/src/cli/commands/oauth/index.ts +62 -10
  82. package/src/cli/commands/oauth/mode.ts +263 -0
  83. package/src/cli/commands/oauth/ping.ts +222 -0
  84. package/src/cli/commands/oauth/providers.ts +30 -3
  85. package/src/cli/commands/oauth/request.ts +576 -0
  86. package/src/cli/commands/oauth/shared.ts +132 -0
  87. package/src/cli/commands/oauth/status.ts +202 -0
  88. package/src/cli/commands/oauth/token.ts +159 -0
  89. package/src/cli/commands/platform.ts +20 -14
  90. package/src/cli.ts +82 -17
  91. package/src/config/assistant-feature-flags.ts +74 -11
  92. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  93. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  94. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  95. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  96. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  97. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  98. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  99. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  100. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  101. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  102. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  103. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  104. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  105. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  106. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  107. package/src/config/bundled-tool-registry.ts +2 -19
  108. package/src/config/env.ts +5 -1
  109. package/src/config/feature-flag-registry.json +57 -41
  110. package/src/config/loader.ts +4 -0
  111. package/src/config/schemas/platform.ts +0 -8
  112. package/src/config/schemas/security.ts +9 -1
  113. package/src/config/schemas/services.ts +1 -1
  114. package/src/config/skill-state.ts +1 -3
  115. package/src/config/skills.ts +2 -4
  116. package/src/credential-execution/feature-gates.ts +9 -16
  117. package/src/credential-execution/process-manager.ts +12 -0
  118. package/src/daemon/config-watcher.ts +4 -0
  119. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  120. package/src/daemon/conversation-agent-loop.ts +49 -2
  121. package/src/daemon/conversation-memory.ts +0 -1
  122. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  123. package/src/daemon/handlers/conversations.ts +41 -33
  124. package/src/daemon/lifecycle.ts +26 -2
  125. package/src/daemon/message-types/acp.ts +0 -15
  126. package/src/daemon/message-types/memory.ts +0 -1
  127. package/src/daemon/message-types/messages.ts +9 -1
  128. package/src/daemon/message-types/schedules.ts +9 -0
  129. package/src/daemon/server.ts +19 -7
  130. package/src/email/feature-gate.ts +3 -3
  131. package/src/heartbeat/heartbeat-service.ts +48 -0
  132. package/src/inbound/platform-callback-registration.ts +61 -7
  133. package/src/mcp/mcp-oauth-provider.ts +3 -3
  134. package/src/memory/app-store.ts +3 -3
  135. package/src/memory/conversation-crud.ts +124 -0
  136. package/src/memory/conversation-title-service.ts +7 -17
  137. package/src/memory/db-init.ts +8 -0
  138. package/src/memory/embedding-local.ts +47 -2
  139. package/src/memory/indexer.ts +13 -10
  140. package/src/memory/items-extractor.ts +12 -4
  141. package/src/memory/job-utils.ts +5 -0
  142. package/src/memory/jobs-store.ts +10 -2
  143. package/src/memory/journal-memory.ts +6 -2
  144. package/src/memory/llm-request-log-store.ts +88 -21
  145. package/src/memory/memory-recall-log-store.ts +128 -0
  146. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  147. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  148. package/src/memory/migrations/index.ts +2 -0
  149. package/src/memory/retriever.test.ts +4 -5
  150. package/src/memory/schema/infrastructure.ts +31 -0
  151. package/src/memory/schema/oauth.ts +3 -0
  152. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  153. package/src/oauth/connect-orchestrator.ts +54 -0
  154. package/src/oauth/manual-token-connection.ts +5 -5
  155. package/src/oauth/oauth-store.ts +26 -5
  156. package/src/oauth/seed-providers.ts +10 -1
  157. package/src/permissions/checker.ts +2 -2
  158. package/src/permissions/trust-client.ts +2 -2
  159. package/src/platform/client.ts +2 -2
  160. package/src/prompts/journal-context.ts +6 -1
  161. package/src/providers/anthropic/client.ts +143 -1
  162. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  163. package/src/runtime/auth/route-policy.ts +0 -1
  164. package/src/runtime/btw-sidechain.ts +7 -1
  165. package/src/runtime/channel-approvals.ts +2 -2
  166. package/src/runtime/channel-readiness-service.ts +30 -7
  167. package/src/runtime/http-router.ts +31 -0
  168. package/src/runtime/http-server.ts +21 -4
  169. package/src/runtime/http-types.ts +2 -0
  170. package/src/runtime/pending-interactions.ts +21 -3
  171. package/src/runtime/routes/acp-routes.ts +46 -28
  172. package/src/runtime/routes/app-management-routes.ts +123 -0
  173. package/src/runtime/routes/app-routes.ts +31 -0
  174. package/src/runtime/routes/approval-routes.ts +108 -3
  175. package/src/runtime/routes/attachment-routes.ts +45 -0
  176. package/src/runtime/routes/avatar-routes.ts +16 -0
  177. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  178. package/src/runtime/routes/btw-routes.ts +20 -0
  179. package/src/runtime/routes/call-routes.ts +81 -0
  180. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  181. package/src/runtime/routes/channel-routes.ts +18 -0
  182. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  183. package/src/runtime/routes/contact-routes.ts +77 -0
  184. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  185. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  186. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  187. package/src/runtime/routes/conversation-routes.ts +115 -38
  188. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  189. package/src/runtime/routes/debug-routes.ts +23 -0
  190. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  191. package/src/runtime/routes/documents-routes.ts +42 -0
  192. package/src/runtime/routes/events-routes.ts +10 -0
  193. package/src/runtime/routes/global-search-routes.ts +35 -0
  194. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  195. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  196. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  197. package/src/runtime/routes/host-bash-routes.ts +16 -1
  198. package/src/runtime/routes/host-cu-routes.ts +23 -1
  199. package/src/runtime/routes/host-file-routes.ts +18 -1
  200. package/src/runtime/routes/identity-routes.ts +35 -0
  201. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  202. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  203. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  204. package/src/runtime/routes/integrations/twilio.ts +32 -22
  205. package/src/runtime/routes/invite-routes.ts +83 -0
  206. package/src/runtime/routes/log-export-routes.ts +14 -0
  207. package/src/runtime/routes/memory-item-routes.ts +99 -1
  208. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  209. package/src/runtime/routes/migration-routes.ts +40 -0
  210. package/src/runtime/routes/notification-routes.ts +20 -0
  211. package/src/runtime/routes/oauth-apps.ts +11 -3
  212. package/src/runtime/routes/pairing-routes.ts +15 -0
  213. package/src/runtime/routes/recording-routes.ts +72 -0
  214. package/src/runtime/routes/schedule-routes.ts +77 -5
  215. package/src/runtime/routes/secret-routes.ts +63 -1
  216. package/src/runtime/routes/settings-routes.ts +90 -0
  217. package/src/runtime/routes/skills-routes.ts +98 -16
  218. package/src/runtime/routes/subagents-routes.ts +38 -3
  219. package/src/runtime/routes/surface-action-routes.ts +66 -24
  220. package/src/runtime/routes/surface-content-routes.ts +20 -0
  221. package/src/runtime/routes/telemetry-routes.ts +12 -0
  222. package/src/runtime/routes/trace-event-routes.ts +25 -0
  223. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  224. package/src/runtime/routes/tts-routes.ts +15 -4
  225. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  226. package/src/runtime/routes/usage-routes.ts +59 -0
  227. package/src/runtime/routes/watch-routes.ts +28 -0
  228. package/src/runtime/routes/work-items-routes.ts +59 -0
  229. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  230. package/src/runtime/routes/workspace-routes.ts +102 -0
  231. package/src/schedule/scheduler.ts +7 -1
  232. package/src/security/AGENTS.md +7 -0
  233. package/src/security/credential-backend.ts +1 -1
  234. package/src/security/encrypted-store.ts +3 -3
  235. package/src/security/oauth2.ts +55 -0
  236. package/src/security/secret-ingress.ts +174 -0
  237. package/src/security/secret-patterns.ts +133 -0
  238. package/src/security/secret-scanner.ts +28 -117
  239. package/src/signals/confirm.ts +12 -8
  240. package/src/signals/user-message.ts +18 -3
  241. package/src/skills/skill-memory.ts +1 -2
  242. package/src/tasks/task-runner.ts +7 -1
  243. package/src/tools/credentials/broker.ts +1 -1
  244. package/src/tools/credentials/metadata-store.ts +1 -1
  245. package/src/tools/credentials/vault.ts +2 -3
  246. package/src/tools/memory/definitions.ts +1 -1
  247. package/src/tools/memory/handlers.test.ts +2 -4
  248. package/src/tools/skills/load.ts +1 -1
  249. package/src/tools/terminal/safe-env.ts +7 -0
  250. package/src/tools/tool-manifest.ts +1 -1
  251. package/src/util/log-redact.ts +9 -34
  252. package/docs/architecture/keychain-broker.md +0 -68
  253. package/src/cli/commands/oauth/platform.ts +0 -525
  254. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  255. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  256. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  257. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  258. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  259. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  260. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  261. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  262. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  263. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
@@ -0,0 +1,312 @@
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
+ const BASE_CONFIG = {
8
+ contextWindow: { maxInputTokens: 100000 },
9
+ services: { inference: { model: "test-model", provider: "test-provider" } },
10
+ };
11
+
12
+ let mockConfig: Record<string, unknown> = {
13
+ secretDetection: {
14
+ enabled: true,
15
+ blockIngress: true,
16
+ },
17
+ ...BASE_CONFIG,
18
+ };
19
+
20
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
21
+
22
+ mock.module("../config/loader.js", () => ({
23
+ getConfig: () => mockConfig,
24
+ loadConfig: () => mockConfig,
25
+ invalidateConfigCache: () => {},
26
+ }));
27
+
28
+ mock.module("../util/logger.js", () => ({
29
+ getLogger: () =>
30
+ new Proxy({} as Record<string, unknown>, {
31
+ get: () => () => {},
32
+ }),
33
+ }));
34
+
35
+ mock.module("../util/platform.js", () => ({
36
+ getRootDir: () => "/tmp/vellum-test-secret-ingress-http",
37
+ getWorkspaceDir: () => "/tmp/vellum-test-secret-ingress-http/workspace",
38
+ }));
39
+
40
+ mock.module("../memory/conversation-key-store.js", () => ({
41
+ getOrCreateConversation: () => ({ conversationId: "conv-test" }),
42
+ getConversationByKey: () => null,
43
+ }));
44
+
45
+ mock.module("../memory/attachments-store.js", () => ({
46
+ getAttachmentsByIds: () => [],
47
+ }));
48
+
49
+ mock.module("../memory/canonical-guardian-store.js", () => ({
50
+ createCanonicalGuardianRequest: () => ({
51
+ id: "canonical-id",
52
+ requestCode: "ABC123",
53
+ }),
54
+ generateCanonicalRequestCode: () => "ABC123",
55
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
56
+ listCanonicalGuardianRequests: () => [],
57
+ listPendingRequestsByConversationScope: () => [],
58
+ }));
59
+
60
+ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
61
+ bridgeConfirmationRequestToGuardian: async () => undefined,
62
+ }));
63
+
64
+ const addMessageMock = mock(
65
+ async (
66
+ _conversationId: string,
67
+ _role: string,
68
+ _content?: string,
69
+ _metadata?: Record<string, unknown>,
70
+ ) => ({
71
+ id: "persisted-msg-id",
72
+ }),
73
+ );
74
+
75
+ mock.module("../memory/conversation-crud.js", () => ({
76
+ addMessage: (
77
+ conversationId: string,
78
+ role: string,
79
+ content: string,
80
+ metadata?: Record<string, unknown>,
81
+ ) => addMessageMock(conversationId, role, content, metadata),
82
+ getMessages: () => [],
83
+ provenanceFromTrustContext: () => undefined,
84
+ setConversationOriginChannelIfUnset: () => {},
85
+ setConversationOriginInterfaceIfUnset: () => {},
86
+ getConversationType: () => undefined,
87
+ getConversationMemoryScopeId: () => undefined,
88
+ }));
89
+
90
+ mock.module("../runtime/local-actor-identity.js", () => ({
91
+ resolveLocalTrustContext: () => ({
92
+ trustClass: "guardian",
93
+ sourceChannel: "vellum",
94
+ }),
95
+ }));
96
+
97
+ mock.module("../runtime/trust-context-resolver.js", () => ({
98
+ resolveTrustContext: () => ({
99
+ trustClass: "guardian",
100
+ sourceChannel: "vellum",
101
+ }),
102
+ withSourceChannel: (sourceChannel: unknown, ctx: unknown) => ({
103
+ ...(ctx as Record<string, unknown>),
104
+ sourceChannel,
105
+ }),
106
+ }));
107
+
108
+ mock.module("../runtime/guardian-reply-router.js", () => ({
109
+ routeGuardianReply: async () => ({
110
+ consumed: false,
111
+ decisionApplied: false,
112
+ type: "not_consumed" as const,
113
+ }),
114
+ }));
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Imports (after mocks)
118
+ // ---------------------------------------------------------------------------
119
+
120
+ import type { AuthContext } from "../runtime/auth/types.js";
121
+ import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
122
+
123
+ const testAuthContext: AuthContext = {
124
+ subject: "actor:self:test-user",
125
+ principalType: "actor",
126
+ assistantId: "self",
127
+ actorPrincipalId: "test-user",
128
+ scopeProfile: "actor_client_v1",
129
+ scopes: new Set([
130
+ "chat.read",
131
+ "chat.write",
132
+ "approval.read",
133
+ "approval.write",
134
+ "settings.read",
135
+ "settings.write",
136
+ "attachments.read",
137
+ "attachments.write",
138
+ "calls.read",
139
+ "calls.write",
140
+ "feature_flags.read",
141
+ "feature_flags.write",
142
+ ]),
143
+ policyEpoch: 1,
144
+ };
145
+
146
+ function makeRequest(body: Record<string, unknown>): Request {
147
+ return new Request("http://localhost/v1/messages", {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/json" },
150
+ body: JSON.stringify({
151
+ conversationKey: "test-conversation",
152
+ sourceChannel: "vellum",
153
+ interface: "macos",
154
+ ...body,
155
+ }),
156
+ });
157
+ }
158
+
159
+ const persistUserMessageMock = mock(async () => "persisted-id");
160
+ const runAgentLoopMock = mock(async () => undefined);
161
+
162
+ function makeSendMessageDeps() {
163
+ const session = {
164
+ setTrustContext: () => {},
165
+ updateClient: () => {},
166
+ emitConfirmationStateChanged: () => {},
167
+ emitActivityState: () => {},
168
+ setTurnChannelContext: () => {},
169
+ setTurnInterfaceContext: () => {},
170
+ ensureActorScopedHistory: async () => {},
171
+ usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
172
+ isProcessing: () => false,
173
+ hasAnyPendingConfirmation: () => false,
174
+ denyAllPendingConfirmations: () => {},
175
+ enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
176
+ persistUserMessage: persistUserMessageMock,
177
+ runAgentLoop: runAgentLoopMock,
178
+ getMessages: () => [] as unknown[],
179
+ assistantId: "self",
180
+ trustContext: undefined,
181
+ hasPendingConfirmation: () => false,
182
+ setHostBashProxy: () => {},
183
+ setHostFileProxy: () => {},
184
+ setHostCuProxy: () => {},
185
+ addPreactivatedSkillId: () => {},
186
+ } as unknown as import("../daemon/conversation.js").Conversation;
187
+
188
+ return {
189
+ sendMessageDeps: {
190
+ getOrCreateConversation: async () => session,
191
+ assistantEventHub: { publish: async () => {} } as any,
192
+ resolveAttachments: () => [],
193
+ },
194
+ };
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Tests
199
+ // ---------------------------------------------------------------------------
200
+
201
+ describe("secret ingress — HTTP route", () => {
202
+ beforeEach(() => {
203
+ mockConfig = {
204
+ secretDetection: {
205
+ enabled: true,
206
+ blockIngress: true,
207
+ },
208
+ ...BASE_CONFIG,
209
+ };
210
+ persistUserMessageMock.mockClear();
211
+ runAgentLoopMock.mockClear();
212
+ addMessageMock.mockClear();
213
+ });
214
+
215
+ test("POST /v1/messages with GitHub token returns 422 secret_blocked", async () => {
216
+ const req = makeRequest({
217
+ content: "Here is my token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234",
218
+ });
219
+
220
+ const res = await handleSendMessage(
221
+ req,
222
+ makeSendMessageDeps(),
223
+ testAuthContext,
224
+ );
225
+ expect(res.status).toBe(422);
226
+
227
+ const body = (await res.json()) as Record<string, unknown>;
228
+ expect(body.error).toBe("secret_blocked");
229
+ expect(body.accepted).toBe(false);
230
+ expect(body.detectedTypes).toContain("GitHub Token");
231
+ });
232
+
233
+ test("POST /v1/messages with normal text returns 202 accepted", async () => {
234
+ const req = makeRequest({
235
+ content: "Hello, can you help me with my project?",
236
+ });
237
+
238
+ const res = await handleSendMessage(
239
+ req,
240
+ makeSendMessageDeps(),
241
+ testAuthContext,
242
+ );
243
+ expect(res.status).toBe(202);
244
+ });
245
+
246
+ test("POST /v1/messages with bypassSecretCheck: true and secret returns 202", async () => {
247
+ const req = makeRequest({
248
+ content: "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234",
249
+ bypassSecretCheck: true,
250
+ });
251
+
252
+ const res = await handleSendMessage(
253
+ req,
254
+ makeSendMessageDeps(),
255
+ testAuthContext,
256
+ );
257
+ expect(res.status).toBe(202);
258
+ });
259
+
260
+ test("POST /v1/messages with JWT eyJ... returns 202 (not in curated patterns)", async () => {
261
+ const req = makeRequest({
262
+ content:
263
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
264
+ });
265
+
266
+ const res = await handleSendMessage(
267
+ req,
268
+ makeSendMessageDeps(),
269
+ testAuthContext,
270
+ );
271
+ expect(res.status).toBe(202);
272
+ });
273
+
274
+ test("POST /v1/messages with blockIngress: false config and secret returns 202", async () => {
275
+ mockConfig = {
276
+ secretDetection: {
277
+ enabled: true,
278
+ blockIngress: false,
279
+ },
280
+ ...BASE_CONFIG,
281
+ };
282
+
283
+ const req = makeRequest({
284
+ content: "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234",
285
+ });
286
+
287
+ const res = await handleSendMessage(
288
+ req,
289
+ makeSendMessageDeps(),
290
+ testAuthContext,
291
+ );
292
+ expect(res.status).toBe(202);
293
+ });
294
+
295
+ test("message is NOT persisted when blocked", async () => {
296
+ const req = makeRequest({
297
+ content: "AKIAIOSFODNN7EXAMPLE",
298
+ });
299
+
300
+ const res = await handleSendMessage(
301
+ req,
302
+ makeSendMessageDeps(),
303
+ testAuthContext,
304
+ );
305
+ expect(res.status).toBe(422);
306
+
307
+ // persistUserMessage should not have been called
308
+ expect(persistUserMessageMock).not.toHaveBeenCalled();
309
+ // addMessage should not have been called
310
+ expect(addMessageMock).not.toHaveBeenCalled();
311
+ });
312
+ });
@@ -0,0 +1,283 @@
1
+ import { afterEach, 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 = {
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
+ info: () => {},
23
+ warn: () => {},
24
+ error: () => {},
25
+ debug: () => {},
26
+ trace: () => {},
27
+ fatal: () => {},
28
+ silent: () => {},
29
+ child: function () {
30
+ return this;
31
+ },
32
+ }),
33
+ }));
34
+
35
+ mock.module("../util/platform.js", () => ({
36
+ getRootDir: () => "/tmp/vellum-test-ingress",
37
+ getWorkspaceDir: () => "/tmp/vellum-test-ingress/workspace",
38
+ }));
39
+
40
+ import { resetAllowlist } from "../security/secret-allowlist.js";
41
+ import { checkIngressForSecrets } from "../security/secret-ingress.js";
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Tests
45
+ // ---------------------------------------------------------------------------
46
+
47
+ describe("checkIngressForSecrets", () => {
48
+ beforeEach(() => {
49
+ mockConfig = {
50
+ secretDetection: {
51
+ enabled: true,
52
+ blockIngress: true,
53
+ },
54
+ };
55
+ resetAllowlist();
56
+ });
57
+
58
+ afterEach(() => {
59
+ resetAllowlist();
60
+ });
61
+
62
+ // ── Blocked patterns ───────────────────────────────────────────────
63
+
64
+ test("blocks Google OAuth secret (GOCSPX-*)", () => {
65
+ const result = checkIngressForSecrets(
66
+ "My client secret is GOCSPX-abcdefghijklmnopqrstuvwxyz12",
67
+ );
68
+ expect(result.blocked).toBe(true);
69
+ expect(result.detectedTypes).toContain("Google OAuth Client Secret");
70
+ expect(result.userNotice).toBeDefined();
71
+ });
72
+
73
+ test("blocks GitHub PAT (ghp_*)", () => {
74
+ const result = checkIngressForSecrets(
75
+ "Here is my token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
76
+ );
77
+ expect(result.blocked).toBe(true);
78
+ expect(result.detectedTypes).toContain("GitHub Token");
79
+ });
80
+
81
+ test("blocks Slack bot token (xoxb-*)", () => {
82
+ const result = checkIngressForSecrets(
83
+ "Use this: xoxb-1234567890-9876543210-AbCdEfGhIjKlMnOpQrStUvWx",
84
+ );
85
+ expect(result.blocked).toBe(true);
86
+ expect(result.detectedTypes).toContain("Slack Bot Token");
87
+ });
88
+
89
+ test("blocks Anthropic API key (sk-ant-*)", () => {
90
+ const key =
91
+ "sk-ant-api03-abcDefGhiJklMnoPqrStuVwxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIj";
92
+ const result = checkIngressForSecrets(`Key: ${key}`);
93
+ expect(result.blocked).toBe(true);
94
+ expect(result.detectedTypes).toContain("Anthropic API Key");
95
+ });
96
+
97
+ test("blocks private key header", () => {
98
+ const result = checkIngressForSecrets(
99
+ "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAK...",
100
+ );
101
+ expect(result.blocked).toBe(true);
102
+ expect(result.detectedTypes).toContain("Private Key");
103
+ });
104
+
105
+ test("blocks AWS access key (AKIA*)", () => {
106
+ const result = checkIngressForSecrets("AWS key: AKIAIOSFODNN7EXAMPLE");
107
+ expect(result.blocked).toBe(true);
108
+ expect(result.detectedTypes).toContain("AWS Access Key");
109
+ });
110
+
111
+ test("blocks Stripe secret key (sk_live_*)", () => {
112
+ const result = checkIngressForSecrets("sk_live_abcdefghijklmnopqrstuvwx");
113
+ expect(result.blocked).toBe(true);
114
+ expect(result.detectedTypes).toContain("Stripe Secret Key");
115
+ });
116
+
117
+ test("blocks SendGrid API key", () => {
118
+ const result = checkIngressForSecrets(
119
+ "SG.abcdefghijklmnopqrstuv.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst",
120
+ );
121
+ expect(result.blocked).toBe(true);
122
+ expect(result.detectedTypes).toContain("SendGrid API Key");
123
+ });
124
+
125
+ test("blocks npm token", () => {
126
+ const result = checkIngressForSecrets(
127
+ "npm_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
128
+ );
129
+ expect(result.blocked).toBe(true);
130
+ expect(result.detectedTypes).toContain("npm Token");
131
+ });
132
+
133
+ test("blocks OpenAI project key (sk-proj-*)", () => {
134
+ const key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCd";
135
+ const result = checkIngressForSecrets(`My key: ${key}`);
136
+ expect(result.blocked).toBe(true);
137
+ expect(result.detectedTypes).toContain("OpenAI Project Key");
138
+ });
139
+
140
+ test("blocks Google API key (AIza*)", () => {
141
+ const result = checkIngressForSecrets(
142
+ "AIzaSyA0123456789abcdefghijklmnopqrstuvw",
143
+ );
144
+ expect(result.blocked).toBe(true);
145
+ expect(result.detectedTypes).toContain("Google API Key");
146
+ });
147
+
148
+ test("blocks GitLab PAT (glpat-*)", () => {
149
+ const result = checkIngressForSecrets("glpat-abcdefghijklmnopqrst");
150
+ expect(result.blocked).toBe(true);
151
+ expect(result.detectedTypes).toContain("GitLab Token");
152
+ });
153
+
154
+ test("blocks Telegram Bot Token", () => {
155
+ const result = checkIngressForSecrets(
156
+ "Bot token: 123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi",
157
+ );
158
+ expect(result.blocked).toBe(true);
159
+ expect(result.detectedTypes).toContain("Telegram Bot Token");
160
+ });
161
+
162
+ test("blocks Twilio API Key (SK*)", () => {
163
+ const result = checkIngressForSecrets(
164
+ "Twilio key: SK0123456789abcdef0123456789abcdef",
165
+ );
166
+ expect(result.blocked).toBe(true);
167
+ expect(result.detectedTypes).toContain("Twilio API Key");
168
+ });
169
+
170
+ // ── Not blocked (excluded patterns) ────────────────────────────────
171
+
172
+ test("does not block normal text", () => {
173
+ const result = checkIngressForSecrets(
174
+ "Hello, can you help me set up my project?",
175
+ );
176
+ expect(result.blocked).toBe(false);
177
+ expect(result.detectedTypes).toHaveLength(0);
178
+ });
179
+
180
+ test("does not block high-entropy hex (40-char git SHA)", () => {
181
+ const result = checkIngressForSecrets(
182
+ "Commit: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
183
+ );
184
+ expect(result.blocked).toBe(false);
185
+ });
186
+
187
+ test("does not block UUID", () => {
188
+ const result = checkIngressForSecrets(
189
+ "ID: 550e8400-e29b-41d4-a716-446655440000",
190
+ );
191
+ expect(result.blocked).toBe(false);
192
+ });
193
+
194
+ test("does not block JWT (eyJ...)", () => {
195
+ const result = checkIngressForSecrets(
196
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
197
+ );
198
+ expect(result.blocked).toBe(false);
199
+ });
200
+
201
+ test("does not block password=mysecretvalue (generic assignment)", () => {
202
+ const result = checkIngressForSecrets(
203
+ 'password=mysecretvalue\nsecret="hello world"',
204
+ );
205
+ expect(result.blocked).toBe(false);
206
+ });
207
+
208
+ test("does not block postgres connection string", () => {
209
+ const result = checkIngressForSecrets(
210
+ "postgres://user:pass@host:5432/mydb",
211
+ );
212
+ expect(result.blocked).toBe(false);
213
+ });
214
+
215
+ // ── Config flags ───────────────────────────────────────────────────
216
+
217
+ test("does not block when secretDetection.enabled is false", () => {
218
+ mockConfig = {
219
+ secretDetection: {
220
+ enabled: false,
221
+ blockIngress: true,
222
+ },
223
+ };
224
+ const result = checkIngressForSecrets(
225
+ "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
226
+ );
227
+ expect(result.blocked).toBe(false);
228
+ });
229
+
230
+ test("does not block when blockIngress is false", () => {
231
+ mockConfig = {
232
+ secretDetection: {
233
+ enabled: true,
234
+ blockIngress: false,
235
+ },
236
+ };
237
+ const result = checkIngressForSecrets(
238
+ "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
239
+ );
240
+ expect(result.blocked).toBe(false);
241
+ });
242
+
243
+ // ── Placeholder / test values ──────────────────────────────────────
244
+
245
+ test("does not block placeholder test keys (sk-test-*)", () => {
246
+ const result = checkIngressForSecrets("sk-test-abc123");
247
+ expect(result.blocked).toBe(false);
248
+ });
249
+
250
+ test("does not block fake_ prefixed values", () => {
251
+ // A GitHub-like token with fake_ prefix
252
+ const result = checkIngressForSecrets(
253
+ "fake_ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
254
+ );
255
+ expect(result.blocked).toBe(false);
256
+ });
257
+
258
+ test("does not block repeated character patterns", () => {
259
+ // AKIA followed by 16 repeated X characters
260
+ const result = checkIngressForSecrets("AKIAXXXXXXXXXXXXXXXX");
261
+ expect(result.blocked).toBe(false);
262
+ });
263
+
264
+ // ── Multiple secrets ───────────────────────────────────────────────
265
+
266
+ test("reports multiple detected types", () => {
267
+ const result = checkIngressForSecrets(
268
+ "AWS: AKIAIOSFODNN7EXAMPLE\nGitHub: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
269
+ );
270
+ expect(result.blocked).toBe(true);
271
+ expect(result.detectedTypes).toContain("AWS Access Key");
272
+ expect(result.detectedTypes).toContain("GitHub Token");
273
+ expect(result.detectedTypes.length).toBe(2);
274
+ });
275
+
276
+ test("user notice does not echo secret values", () => {
277
+ const secret = "AKIAIOSFODNN7EXAMPLE";
278
+ const result = checkIngressForSecrets(`Key: ${secret}`);
279
+ expect(result.blocked).toBe(true);
280
+ expect(result.userNotice).toBeDefined();
281
+ expect(result.userNotice!).not.toContain(secret);
282
+ });
283
+ });
@@ -47,7 +47,7 @@ mock.module("../util/logger.js", () => ({
47
47
  }),
48
48
  }));
49
49
 
50
- // Track keychain writes
50
+ // Track credential store writes
51
51
  const storedKeys = new Map<string, string>();
52
52
  mock.module("../security/secure-keys.js", () => {
53
53
  const syncSet = (key: string, value: string) => {
@@ -112,7 +112,7 @@ describe("one-time send override", () => {
112
112
  );
113
113
  expect(result.isError).toBe(true);
114
114
  expect(result.content).toContain("not enabled");
115
- // Value must NOT be stored in keychain
115
+ // Value must NOT be stored in credential store
116
116
  expect(storedKeys.has(credentialKey("svc", "key"))).toBe(false);
117
117
  });
118
118
 
@@ -134,11 +134,11 @@ describe("one-time send override", () => {
134
134
  );
135
135
  expect(result.isError).toBe(false);
136
136
  expect(result.content).toContain("NOT saved");
137
- // Value must NOT be stored in keychain
137
+ // Value must NOT be stored in credential store
138
138
  expect(storedKeys.has(credentialKey("svc", "key"))).toBe(false);
139
139
  });
140
140
 
141
- test("store delivery always persists to keychain regardless of allowOneTimeSend", async () => {
141
+ test("store delivery always persists to credential store regardless of allowOneTimeSend", async () => {
142
142
  mockConfig.secretDetection.allowOneTimeSend = true;
143
143
  const context = {
144
144
  workingDir: "/tmp",
@@ -137,7 +137,7 @@ describe("frontmatter feature-flag integration", () => {
137
137
  expect(skill!.featureFlag).toBe("contacts");
138
138
 
139
139
  const key = skillFlagKey(skill!);
140
- expect(key).toBe("feature_flags.contacts.enabled");
140
+ expect(key).toBe("contacts");
141
141
  });
142
142
 
143
143
  test("skillFlagKey returns undefined for skill without feature-flag", () => {
@@ -162,7 +162,7 @@ describe("frontmatter feature-flag integration", () => {
162
162
 
163
163
  test("resolveSkillStates includes skill with featureFlag when flag is ON", () => {
164
164
  _setOverridesForTesting({
165
- "feature_flags.contacts.enabled": true,
165
+ contacts: true,
166
166
  });
167
167
  const skill = buildSkillSummary("contacts", SKILL_MD_WITH_FLAG)!;
168
168
  const config = makeConfig();
@@ -176,7 +176,7 @@ describe("frontmatter feature-flag integration", () => {
176
176
  const skill = buildSkillSummary("plain-skill", SKILL_MD_WITHOUT_FLAG)!;
177
177
  // Even with an explicit false override for this skill ID, it should pass through
178
178
  _setOverridesForTesting({
179
- "feature_flags.plain-skill.enabled": false,
179
+ "plain-skill": false,
180
180
  });
181
181
  const config = makeConfig();
182
182
 
@@ -200,7 +200,7 @@ describe("frontmatter feature-flag integration", () => {
200
200
 
201
201
  // Step 3: Derive the flag key
202
202
  const key = skillFlagKey(skill);
203
- expect(key).toBe("feature_flags.contacts.enabled");
203
+ expect(key).toBe("contacts");
204
204
 
205
205
  // Step 4: Check flag state — "contacts" has defaultEnabled: true in registry
206
206
  const configDefault = makeConfig();