@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
@@ -73,6 +73,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
73
73
 
74
74
  "integration:slack": {
75
75
  service: "integration:slack",
76
+ loopbackPort: 17322,
76
77
  injectionTemplates: [
77
78
  {
78
79
  hostPattern: "slack.com",
@@ -114,6 +115,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
114
115
 
115
116
  "integration:notion": {
116
117
  service: "integration:notion",
118
+ loopbackPort: 17323,
117
119
  injectionTemplates: [
118
120
  {
119
121
  hostPattern: "api.notion.com",
@@ -225,6 +227,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
225
227
 
226
228
  "integration:linear": {
227
229
  service: "integration:linear",
230
+ loopbackPort: 17324,
228
231
  injectionTemplates: [
229
232
  {
230
233
  hostPattern: "api.linear.app",
@@ -305,6 +308,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
305
308
 
306
309
  "integration:todoist": {
307
310
  service: "integration:todoist",
311
+ loopbackPort: 17325,
308
312
  injectionTemplates: [
309
313
  {
310
314
  hostPattern: "api.todoist.com",
@@ -347,6 +351,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
347
351
 
348
352
  "integration:discord": {
349
353
  service: "integration:discord",
354
+ loopbackPort: 17326,
350
355
  injectionTemplates: [
351
356
  {
352
357
  hostPattern: "discord.com",
@@ -385,6 +390,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
385
390
 
386
391
  "integration:dropbox": {
387
392
  service: "integration:dropbox",
393
+ loopbackPort: 17327,
388
394
  injectionTemplates: [
389
395
  {
390
396
  hostPattern: "api.dropboxapi.com",
@@ -433,6 +439,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
433
439
 
434
440
  "integration:asana": {
435
441
  service: "integration:asana",
442
+ loopbackPort: 17328,
436
443
  injectionTemplates: [
437
444
  {
438
445
  hostPattern: "app.asana.com",
@@ -470,6 +477,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
470
477
 
471
478
  "integration:airtable": {
472
479
  service: "integration:airtable",
480
+ loopbackPort: 17329,
473
481
  injectionTemplates: [
474
482
  {
475
483
  hostPattern: "api.airtable.com",
@@ -505,6 +513,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
505
513
 
506
514
  "integration:hubspot": {
507
515
  service: "integration:hubspot",
516
+ loopbackPort: 17330,
508
517
  injectionTemplates: [
509
518
  {
510
519
  hostPattern: "api.hubapi.com",
@@ -543,6 +552,7 @@ export const PROVIDER_BEHAVIORS: Record<string, OAuthProviderBehavior> = {
543
552
 
544
553
  "integration:figma": {
545
554
  service: "integration:figma",
555
+ loopbackPort: 17331,
546
556
  injectionTemplates: [
547
557
  {
548
558
  hostPattern: "api.figma.com",
@@ -5,8 +5,8 @@ import { seedProviders } from "./oauth-store.js";
5
5
  *
6
6
  * These values are upserted into the `oauth_providers` SQLite table on
7
7
  * every startup. Only Vellum implementation fields (authUrl, tokenUrl,
8
- * tokenEndpointAuthMethod, extraParams, callbackTransport, loopbackPort,
9
- * pingUrl) are overwritten on subsequent startups — user-customizable
8
+ * tokenEndpointAuthMethod, extraParams, callbackTransport, pingUrl) are
9
+ * overwritten on subsequent startups — user-customizable
10
10
  * fields (defaultScopes, scopePolicy, userinfoUrl, baseUrl) are only
11
11
  * written on initial insert and preserved across restarts.
12
12
  *
@@ -32,7 +32,6 @@ const PROVIDER_SEED_DATA: Record<
32
32
  };
33
33
  extraParams?: Record<string, string>;
34
34
  callbackTransport?: string;
35
- loopbackPort?: number;
36
35
  }
37
36
  > = {
38
37
  "integration:google": {
@@ -94,7 +93,7 @@ const PROVIDER_SEED_DATA: Record<
94
93
  "channels:read,channels:history,groups:read,groups:history,im:read,im:history,im:write,mpim:read,mpim:history,users:read,chat:write,search:read,reactions:write",
95
94
  },
96
95
  callbackTransport: "loopback",
97
- loopbackPort: 17322,
96
+
98
97
  },
99
98
 
100
99
  "integration:notion": {
@@ -111,7 +110,8 @@ const PROVIDER_SEED_DATA: Record<
111
110
  },
112
111
  extraParams: { owner: "user" },
113
112
  tokenEndpointAuthMethod: "client_secret_basic",
114
- callbackTransport: "gateway",
113
+ callbackTransport: "loopback",
114
+
115
115
  },
116
116
 
117
117
  "integration:twitter": {
@@ -169,6 +169,7 @@ const PROVIDER_SEED_DATA: Record<
169
169
  },
170
170
  extraParams: { prompt: "consent" },
171
171
  callbackTransport: "loopback",
172
+
172
173
  },
173
174
 
174
175
  "integration:spotify": {
@@ -209,6 +210,7 @@ const PROVIDER_SEED_DATA: Record<
209
210
  forbiddenScopes: ["data:delete"],
210
211
  },
211
212
  callbackTransport: "loopback",
213
+
212
214
  },
213
215
 
214
216
  "integration:discord": {
@@ -229,6 +231,7 @@ const PROVIDER_SEED_DATA: Record<
229
231
  forbiddenScopes: [],
230
232
  },
231
233
  callbackTransport: "loopback",
234
+
232
235
  },
233
236
 
234
237
  "integration:dropbox": {
@@ -250,6 +253,7 @@ const PROVIDER_SEED_DATA: Record<
250
253
  },
251
254
  extraParams: { token_access_type: "offline" },
252
255
  callbackTransport: "loopback",
256
+
253
257
  },
254
258
 
255
259
  "integration:asana": {
@@ -265,6 +269,7 @@ const PROVIDER_SEED_DATA: Record<
265
269
  forbiddenScopes: [],
266
270
  },
267
271
  callbackTransport: "loopback",
272
+
268
273
  },
269
274
 
270
275
  "integration:airtable": {
@@ -285,6 +290,7 @@ const PROVIDER_SEED_DATA: Record<
285
290
  },
286
291
  tokenEndpointAuthMethod: "client_secret_post",
287
292
  callbackTransport: "loopback",
293
+
288
294
  },
289
295
 
290
296
  "integration:hubspot": {
@@ -309,6 +315,7 @@ const PROVIDER_SEED_DATA: Record<
309
315
  forbiddenScopes: [],
310
316
  },
311
317
  callbackTransport: "loopback",
318
+
312
319
  },
313
320
 
314
321
  "integration:figma": {
@@ -324,6 +331,7 @@ const PROVIDER_SEED_DATA: Record<
324
331
  forbiddenScopes: [],
325
332
  },
326
333
  callbackTransport: "loopback",
334
+
327
335
  },
328
336
 
329
337
  // Manual-token providers: these don't use OAuth2 flows but need provider
@@ -143,7 +143,6 @@ const LOW_RISK_PROGRAMS = new Set([
143
143
  "tree",
144
144
  "du",
145
145
  "df",
146
- "assistant",
147
146
  ]);
148
147
 
149
148
  // High-risk shell programs / patterns
@@ -197,6 +196,15 @@ const LOW_RISK_GIT_SUBCOMMANDS = new Set([
197
196
  "reflog",
198
197
  ]);
199
198
 
199
+ // Vellum/assistant CLI subcommands that are low-risk (read-only)
200
+ const LOW_RISK_CLI_SUBCOMMANDS = new Set([
201
+ "ps",
202
+ "doctor",
203
+ "audit",
204
+ "completions",
205
+ "map",
206
+ ]);
207
+
200
208
  // Commands that wrap another program — the real program appears as the first
201
209
  // non-flag argument. When one of these is the segment program we look through
202
210
  // its args to find the effective program (e.g. `env curl …` → curl).
@@ -649,6 +657,17 @@ async function classifyRiskUncached(
649
657
  continue;
650
658
  }
651
659
 
660
+ if (prog === "vellum" || prog === "assistant") {
661
+ const subcommand = seg.args[0];
662
+ if (subcommand && LOW_RISK_CLI_SUBCOMMANDS.has(subcommand)) {
663
+ // Read-only subcommands stay at current risk
664
+ continue;
665
+ }
666
+ // Mutating subcommands are medium
667
+ maxRisk = RiskLevel.Medium;
668
+ continue;
669
+ }
670
+
652
671
  if (!LOW_RISK_PROGRAMS.has(prog)) {
653
672
  // Unknown program → medium
654
673
  if (maxRisk === RiskLevel.Low) {
@@ -29,7 +29,7 @@ describe("buildCliReferenceSection", () => {
29
29
 
30
30
  test("mentions bash as the way to invoke the CLI", () => {
31
31
  const result = buildCliReferenceSection();
32
- expect(result).toContain("available via `bash`");
32
+ expect(result).toContain("use the `bash` tool");
33
33
  });
34
34
 
35
35
  test("routes account and auth work through documented assistant CLI commands", () => {
@@ -309,7 +309,7 @@ export function buildStarterTaskPlaybookSection(): string {
309
309
  '3. Let the user pick one. Accept color names, hex values, or descriptions (e.g. "something warm").',
310
310
  '4. Confirm the selection: "I\'ll set your accent color to **{label}** ({hex}). Sound good?"',
311
311
  "5. On confirmation:",
312
- ' - Use `app_file_edit` to update the `## Dashboard Color Preference` section in USER.md with `label`, `hex`, `source: "user_selected"`, and `applied: true`.',
312
+ ' - Use `app_file_edit` to update the `## Color Preference` section in USER.md with `label`, `hex`, and `source: "user_selected"`.',
313
313
  " - Use `app_file_edit` to update the `## Onboarding Tasks` section: set `make_it_yours` to `done`.",
314
314
  "6. If the user declines or wants to skip, set `make_it_yours` to `skipped` in USER.md and move on.",
315
315
  "",
@@ -511,17 +511,8 @@ export function buildChannelAwarenessSection(): string {
511
511
  "- When the user asks about voice input or push-to-talk settings, use the tool to apply changes directly rather than directing them to settings.",
512
512
  "- When `microphone_permission_granted` is `false`, guide the user to grant microphone access in System Settings before using voice features.",
513
513
  "",
514
- "### Group chat etiquette",
515
- "- In group chats, you are a **participant**, not the user's proxy. Think before you speak.",
516
- "- **Respond when:** directly mentioned, you can add genuine value, something witty fits naturally, or correcting important misinformation.",
517
- '- **Stay silent when:** it\'s casual banter between humans, someone already answered, your response would just be "yeah" or "nice", or the conversation flows fine without you.',
518
- "- **The human rule:** humans don't respond to every message in a group chat. Neither should you. Quality over quantity.",
519
- "- On platforms with reactions (Discord, Slack), use emoji reactions naturally to acknowledge without cluttering.",
520
- "",
521
514
  "### Platform formatting",
522
- "- **Discord/WhatsApp:** Do not use markdown tables — use bullet lists instead.",
523
- "- **Discord links:** Wrap multiple links in `<>` to suppress embeds.",
524
- "- **WhatsApp:** No markdown headers — use **bold** or CAPS for emphasis.",
515
+ "- **WhatsApp:** Do not use markdown tables — use bullet lists instead. No markdown headers — use **bold** or CAPS for emphasis.",
525
516
  ].join("\n");
526
517
  }
527
518
 
@@ -66,10 +66,8 @@ Do NOT delete this file until ALL of the following are true:
66
66
 
67
67
  - You have a name (given by user or self-chosen)
68
68
  - You've figured out your vibe and adopted it
69
- - 2 suggestions shown (via `ui_show` or as text if UI unavailable)
70
- - The user selected one, deferred both, or typed an alternate direction
71
69
 
72
- Once every condition is met, delete this file. You're done here.
70
+ Once every condition is met, delete this file. You're done here. If you still haven't shown the 2 suggestions from step 6, do that in the same turn before or after deleting.
73
71
 
74
72
  ---
75
73
 
@@ -107,7 +107,6 @@ function buildSyntheticToolResult(
107
107
  };
108
108
  }
109
109
 
110
-
111
110
  /**
112
111
  * Collect ordered IDs of client-side tool_use blocks only.
113
112
  * Server-side tools (server_tool_use / web_search_tool_result) are self-paired
@@ -228,7 +227,10 @@ function normalizeFollowingUserContent(
228
227
  toolResultPrefix: orderedResults,
229
228
  remainingContent: remaining,
230
229
  missingIds,
231
- hadOrderedPrefix: hasOrderedToolResultPrefix(nextContent, orderedToolUseIds),
230
+ hadOrderedPrefix: hasOrderedToolResultPrefix(
231
+ nextContent,
232
+ orderedToolUseIds,
233
+ ),
232
234
  };
233
235
  }
234
236
 
@@ -712,20 +714,26 @@ export class AnthropicProvider implements Provider {
712
714
  type: "server_tool_start",
713
715
  name: event.content_block.name,
714
716
  toolUseId: event.content_block.id,
715
- input: (
716
- event.content_block as { input?: Record<string, unknown> }
717
- ).input ?? {},
717
+ input:
718
+ (event.content_block as { input?: Record<string, unknown> })
719
+ .input ?? {},
718
720
  });
719
721
  }
720
722
  if (
721
723
  event.type === "content_block_start" &&
722
724
  event.content_block.type === "web_search_tool_result"
723
725
  ) {
726
+ const block = event.content_block as {
727
+ tool_use_id: string;
728
+ content?: { type: "web_search_tool_result_error" } | unknown[];
729
+ };
730
+ const isError =
731
+ !Array.isArray(block.content) &&
732
+ block.content?.type === "web_search_tool_result_error";
724
733
  onEvent?.({
725
734
  type: "server_tool_complete",
726
- toolUseId: (
727
- event.content_block as { tool_use_id: string }
728
- ).tool_use_id,
735
+ toolUseId: block.tool_use_id,
736
+ isError: !!isError,
729
737
  });
730
738
  }
731
739
  if (event.type === "content_block_stop") {
@@ -29,7 +29,7 @@ export const MANAGED_PROVIDER_META: Record<string, ManagedProviderMeta> = {
29
29
  anthropic: {
30
30
  name: "anthropic",
31
31
  managed: true,
32
- proxyPath: "/v1/runtime-proxy/vertex",
32
+ proxyPath: "/v1/runtime-proxy/anthropic",
33
33
  },
34
34
  gemini: {
35
35
  name: "gemini",
@@ -1,4 +1,5 @@
1
1
  import { wrapWithLogfire } from "../logfire.js";
2
+ import { getSecureKey } from "../security/secure-keys.js";
2
3
  import { ConfigError, ProviderNotConfiguredError } from "../util/errors.js";
3
4
  import { AnthropicProvider } from "./anthropic/client.js";
4
5
  import { FailoverProvider, type ProviderHealthStatus } from "./failover.js";
@@ -135,7 +136,6 @@ export function getDefaultModel(providerName: string): string {
135
136
  }
136
137
 
137
138
  export interface ProvidersConfig {
138
- apiKeys: Record<string, string>;
139
139
  provider: string;
140
140
  model: string;
141
141
  webSearchProvider?: string;
@@ -211,13 +211,14 @@ export function initializeProviders(config: ProvidersConfig): void {
211
211
  const streamTimeoutMs =
212
212
  (config.timeouts?.providerStreamTimeoutSec ?? 300) * 1000;
213
213
 
214
- if (config.apiKeys.anthropic) {
214
+ const anthropicKey = getSecureKey("anthropic");
215
+ if (anthropicKey) {
215
216
  const model = resolveModel(config, "anthropic");
216
217
  registerProvider(
217
218
  "anthropic",
218
219
  new RetryProvider(
219
220
  wrapWithLogfire(
220
- new AnthropicProvider(config.apiKeys.anthropic, model, {
221
+ new AnthropicProvider(anthropicKey, model, {
221
222
  useNativeWebSearch: config.webSearchProvider === "anthropic-native",
222
223
  streamTimeoutMs,
223
224
  }),
@@ -226,8 +227,8 @@ export function initializeProviders(config: ProvidersConfig): void {
226
227
  );
227
228
  routingSources.set("anthropic", "user-key");
228
229
  } else {
229
- // No user Anthropic key — route through Vertex managed proxy
230
- const managedBaseUrl = buildManagedBaseUrl("vertex");
230
+ // No user Anthropic key — route through managed proxy
231
+ const managedBaseUrl = buildManagedBaseUrl("anthropic");
231
232
  if (managedBaseUrl) {
232
233
  const ctx = resolveManagedProxyContext();
233
234
  const model = resolveModel(config, "anthropic");
@@ -247,13 +248,14 @@ export function initializeProviders(config: ProvidersConfig): void {
247
248
  routingSources.set("anthropic", "managed-proxy");
248
249
  }
249
250
  }
250
- if (config.apiKeys.openai) {
251
+ const openaiKey = getSecureKey("openai");
252
+ if (openaiKey) {
251
253
  const model = resolveModel(config, "openai");
252
254
  registerProvider(
253
255
  "openai",
254
256
  new RetryProvider(
255
257
  wrapWithLogfire(
256
- new OpenAIProvider(config.apiKeys.openai, model, { streamTimeoutMs }),
258
+ new OpenAIProvider(openaiKey, model, { streamTimeoutMs }),
257
259
  ),
258
260
  ),
259
261
  );
@@ -277,13 +279,14 @@ export function initializeProviders(config: ProvidersConfig): void {
277
279
  routingSources.set("openai", "managed-proxy");
278
280
  }
279
281
  }
280
- if (config.apiKeys.gemini) {
282
+ const geminiKey = getSecureKey("gemini");
283
+ if (geminiKey) {
281
284
  const model = resolveModel(config, "gemini");
282
285
  registerProvider(
283
286
  "gemini",
284
287
  new RetryProvider(
285
288
  wrapWithLogfire(
286
- new GeminiProvider(config.apiKeys.gemini, model, { streamTimeoutMs }),
289
+ new GeminiProvider(geminiKey, model, { streamTimeoutMs }),
287
290
  ),
288
291
  ),
289
292
  );
@@ -308,14 +311,15 @@ export function initializeProviders(config: ProvidersConfig): void {
308
311
  routingSources.set("gemini", "managed-proxy");
309
312
  }
310
313
  }
311
- if (config.provider === "ollama" || config.apiKeys.ollama) {
314
+ const ollamaKey = getSecureKey("ollama");
315
+ if (config.provider === "ollama" || ollamaKey) {
312
316
  const model = resolveModel(config, "ollama");
313
317
  registerProvider(
314
318
  "ollama",
315
319
  new RetryProvider(
316
320
  wrapWithLogfire(
317
321
  new OllamaProvider(model, {
318
- apiKey: config.apiKeys.ollama,
322
+ apiKey: ollamaKey ?? undefined,
319
323
  streamTimeoutMs,
320
324
  }),
321
325
  ),
@@ -323,13 +327,14 @@ export function initializeProviders(config: ProvidersConfig): void {
323
327
  );
324
328
  routingSources.set("ollama", "user-key");
325
329
  }
326
- if (config.apiKeys.fireworks) {
330
+ const fireworksKey = getSecureKey("fireworks");
331
+ if (fireworksKey) {
327
332
  const model = resolveModel(config, "fireworks");
328
333
  registerProvider(
329
334
  "fireworks",
330
335
  new RetryProvider(
331
336
  wrapWithLogfire(
332
- new FireworksProvider(config.apiKeys.fireworks, model, {
337
+ new FireworksProvider(fireworksKey, model, {
333
338
  streamTimeoutMs,
334
339
  }),
335
340
  ),
@@ -355,13 +360,14 @@ export function initializeProviders(config: ProvidersConfig): void {
355
360
  routingSources.set("fireworks", "managed-proxy");
356
361
  }
357
362
  }
358
- if (config.apiKeys.openrouter) {
363
+ const openrouterKey = getSecureKey("openrouter");
364
+ if (openrouterKey) {
359
365
  const model = resolveModel(config, "openrouter");
360
366
  registerProvider(
361
367
  "openrouter",
362
368
  new RetryProvider(
363
369
  wrapWithLogfire(
364
- new OpenRouterProvider(config.apiKeys.openrouter, model, {
370
+ new OpenRouterProvider(openrouterKey, model, {
365
371
  streamTimeoutMs,
366
372
  }),
367
373
  ),
@@ -123,7 +123,7 @@ export type ProviderEvent =
123
123
  toolUseId: string;
124
124
  input: Record<string, unknown>;
125
125
  }
126
- | { type: "server_tool_complete"; toolUseId: string };
126
+ | { type: "server_tool_complete"; toolUseId: string; isError: boolean };
127
127
 
128
128
  export interface SendMessageConfig {
129
129
  model?: string;
@@ -413,6 +413,10 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
413
413
  // OAuth / integrations
414
414
  { endpoint: "integrations/oauth/start", scopes: ["settings.write"] },
415
415
 
416
+ // Ingress config
417
+ { endpoint: "integrations/ingress/config:GET", scopes: ["settings.read"] },
418
+ { endpoint: "integrations/ingress/config", scopes: ["settings.write"] },
419
+
416
420
  // Workspace files
417
421
  { endpoint: "workspace-files", scopes: ["settings.read"] },
418
422
  { endpoint: "workspace-files/read", scopes: ["settings.read"] },
@@ -17,8 +17,11 @@ import {
17
17
  setNestedValue,
18
18
  } from "../../config/loader.js";
19
19
  import { credentialKey } from "../../security/credential-key.js";
20
- import { getSecureKey } from "../../security/secure-keys.js";
21
- import { getTelegramBotUsername } from "../../telegram/bot-username.js";
20
+ import { getSecureKeyAsync } from "../../security/secure-keys.js";
21
+ import {
22
+ getTelegramBotId,
23
+ getTelegramBotUsername,
24
+ } from "../../telegram/bot-username.js";
22
25
  import { getLogger } from "../../util/logger.js";
23
26
  import type {
24
27
  ChannelInviteAdapter,
@@ -37,11 +40,11 @@ import type {
37
40
  * gap so that invite share links can be generated.
38
41
  */
39
42
  export async function ensureTelegramBotUsernameResolved(): Promise<void> {
40
- if (getTelegramBotUsername()) {
41
- return; // Username already cached in config
43
+ if (getTelegramBotUsername() && getTelegramBotId()) {
44
+ return; // Username and bot ID already cached in config
42
45
  }
43
46
 
44
- const token = getSecureKey(credentialKey("telegram", "bot_token"));
47
+ const token = await getSecureKeyAsync(credentialKey("telegram", "bot_token"));
45
48
  if (!token) return;
46
49
 
47
50
  try {
@@ -64,7 +67,7 @@ export async function ensureTelegramBotUsernameResolved(): Promise<void> {
64
67
  }
65
68
  const body = (await res.json()) as {
66
69
  ok: boolean;
67
- result?: { username?: string };
70
+ result?: { id?: number; username?: string };
68
71
  };
69
72
  const username = body.result?.username;
70
73
  if (!username) {
@@ -75,6 +78,9 @@ export async function ensureTelegramBotUsernameResolved(): Promise<void> {
75
78
  }
76
79
  // Write to config
77
80
  const raw = loadRawConfig();
81
+ if (body.result?.id != null) {
82
+ setNestedValue(raw, "telegram.botId", String(body.result.id));
83
+ }
78
84
  setNestedValue(raw, "telegram.botUsername", username);
79
85
  saveRawConfig(raw);
80
86
  invalidateConfigCache();
@@ -170,6 +170,11 @@ export async function sweepFailedEvents(
170
170
  sourceMetadata.uxBrief.trim().length > 0
171
171
  ? sourceMetadata.uxBrief.trim()
172
172
  : undefined;
173
+ const metadataChatType =
174
+ typeof sourceMetadata?.chatType === "string" &&
175
+ sourceMetadata.chatType.trim().length > 0
176
+ ? sourceMetadata.chatType.trim()
177
+ : undefined;
173
178
 
174
179
  try {
175
180
  const { messageId: userMessageId } = await processMessage(
@@ -181,6 +186,7 @@ export async function sweepFailedEvents(
181
186
  channelId: sourceChannel,
182
187
  hints: metadataHints.length > 0 ? metadataHints : undefined,
183
188
  uxBrief: metadataUxBrief,
189
+ chatType: metadataChatType,
184
190
  },
185
191
  assistantId,
186
192
  trustContext,
@@ -116,6 +116,7 @@ export interface RuntimeMessageSessionOptions {
116
116
  channelId: ChannelId;
117
117
  hints?: string[];
118
118
  uxBrief?: string;
119
+ chatType?: string;
119
120
  };
120
121
  assistantId?: string;
121
122
  trustContext?: TrustContext;
@@ -32,10 +32,9 @@ export async function withErrorHandling(
32
32
  }
33
33
  if (err instanceof ProviderNotConfiguredError) {
34
34
  log.warn({ err, endpoint }, "No LLM provider configured");
35
- const envVar = `${err.requestedProvider.toUpperCase()}_API_KEY`;
36
35
  return httpError(
37
36
  "UNPROCESSABLE_ENTITY",
38
- `No API key configured. Set ${envVar} in your environment or run \`vellum hatch\` to set up your assistant.`,
37
+ `No API key configured for ${err.requestedProvider}. Run \`keys set ${err.requestedProvider} <key>\` or configure it from the Settings page under API Keys.`,
39
38
  422,
40
39
  );
41
40
  }
@@ -862,6 +862,7 @@ export function appManagementRouteDefinitions(): RouteDefinition[] {
862
862
  try {
863
863
  const result = await packageApp(params.id);
864
864
  return Response.json({
865
+ type: "bundle_app_response",
865
866
  bundlePath: result.bundlePath,
866
867
  iconImageBase64: result.iconImageBase64,
867
868
  manifest: result.manifest,
@@ -14,6 +14,7 @@ import {
14
14
  createTimeout,
15
15
  userMessage,
16
16
  } from "../../providers/provider-send-message.js";
17
+ import { checkIngressForSecrets } from "../../security/secret-ingress.js";
17
18
  import { getLogger } from "../../util/logger.js";
18
19
  import type { AuthContext } from "../auth/types.js";
19
20
  import { httpError } from "../http-errors.js";
@@ -53,6 +54,24 @@ async function handleBtw(
53
54
  );
54
55
  }
55
56
 
57
+ const trimmedContent = content.trim();
58
+ const ingressCheck = checkIngressForSecrets(trimmedContent);
59
+ if (ingressCheck.blocked) {
60
+ log.warn(
61
+ { detectedTypes: ingressCheck.detectedTypes },
62
+ "Blocked /v1/btw message containing secrets",
63
+ );
64
+ return Response.json(
65
+ {
66
+ accepted: false,
67
+ error: "secret_blocked",
68
+ message: ingressCheck.userNotice,
69
+ detectedTypes: ingressCheck.detectedTypes,
70
+ },
71
+ { status: 422 },
72
+ );
73
+ }
74
+
56
75
  // Look up an existing conversation — never create one. BTW is ephemeral
57
76
  // (the file header promises "No messages are persisted"), so we must not
58
77
  // call getOrCreateConversation which would insert a DB row. When no
@@ -63,7 +82,7 @@ async function handleBtw(
63
82
  const sessionId = mapping?.conversationId ?? conversationKey;
64
83
  const session = await deps.sendMessageDeps.getOrCreateSession(sessionId);
65
84
 
66
- const messages = [...session.getMessages(), userMessage(content.trim())];
85
+ const messages = [...session.getMessages(), userMessage(trimmedContent)];
67
86
  const tools = buildToolDefinitions();
68
87
  const { signal: timeoutSignal, cleanup: cleanupTimeout } =
69
88
  createTimeout(30_000);