vellum 0.2.12 → 0.2.14

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 (209) hide show
  1. package/README.md +32 -0
  2. package/bun.lock +2 -2
  3. package/docs/skills.md +4 -4
  4. package/package.json +2 -2
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
  6. package/src/__tests__/app-git-history.test.ts +176 -0
  7. package/src/__tests__/app-git-service.test.ts +169 -0
  8. package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
  9. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
  10. package/src/__tests__/browser-skill-endstate.test.ts +6 -6
  11. package/src/__tests__/call-bridge.test.ts +105 -13
  12. package/src/__tests__/call-domain.test.ts +163 -0
  13. package/src/__tests__/call-orchestrator.test.ts +171 -0
  14. package/src/__tests__/call-routes-http.test.ts +246 -6
  15. package/src/__tests__/channel-approval-routes.test.ts +438 -0
  16. package/src/__tests__/channel-approval.test.ts +266 -0
  17. package/src/__tests__/channel-approvals.test.ts +393 -0
  18. package/src/__tests__/channel-delivery-store.test.ts +447 -0
  19. package/src/__tests__/checker.test.ts +607 -1048
  20. package/src/__tests__/cli.test.ts +1 -56
  21. package/src/__tests__/config-schema.test.ts +402 -5
  22. package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
  23. package/src/__tests__/conflict-policy.test.ts +121 -0
  24. package/src/__tests__/conflict-store.test.ts +2 -0
  25. package/src/__tests__/contacts-tools.test.ts +3 -3
  26. package/src/__tests__/contradiction-checker.test.ts +99 -1
  27. package/src/__tests__/credential-security-invariants.test.ts +22 -6
  28. package/src/__tests__/credential-vault-unit.test.ts +780 -0
  29. package/src/__tests__/elevenlabs-client.test.ts +271 -0
  30. package/src/__tests__/ephemeral-permissions.test.ts +73 -23
  31. package/src/__tests__/filesystem-tools.test.ts +579 -0
  32. package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
  33. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
  34. package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
  35. package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
  36. package/src/__tests__/handlers-slack-config.test.ts +2 -1
  37. package/src/__tests__/handlers-telegram-config.test.ts +855 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +141 -1
  39. package/src/__tests__/hooks-runner.test.ts +6 -2
  40. package/src/__tests__/host-file-edit-tool.test.ts +124 -0
  41. package/src/__tests__/host-file-read-tool.test.ts +62 -0
  42. package/src/__tests__/host-file-write-tool.test.ts +59 -0
  43. package/src/__tests__/host-shell-tool.test.ts +251 -0
  44. package/src/__tests__/ingress-reconcile.test.ts +581 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +100 -41
  46. package/src/__tests__/ipc-validate.test.ts +50 -0
  47. package/src/__tests__/key-migration.test.ts +23 -0
  48. package/src/__tests__/memory-regressions.test.ts +99 -0
  49. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  50. package/src/__tests__/oauth-callback-registry.test.ts +11 -4
  51. package/src/__tests__/playbook-execution.test.ts +502 -0
  52. package/src/__tests__/playbook-tools.test.ts +4 -6
  53. package/src/__tests__/public-ingress-urls.test.ts +34 -0
  54. package/src/__tests__/qdrant-manager.test.ts +267 -0
  55. package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
  56. package/src/__tests__/recurrence-engine.test.ts +9 -0
  57. package/src/__tests__/recurrence-types.test.ts +8 -0
  58. package/src/__tests__/registry.test.ts +1 -1
  59. package/src/__tests__/runtime-runs.test.ts +1 -25
  60. package/src/__tests__/schedule-store.test.ts +16 -14
  61. package/src/__tests__/schedule-tools.test.ts +83 -0
  62. package/src/__tests__/scheduler-recurrence.test.ts +111 -10
  63. package/src/__tests__/secret-allowlist.test.ts +18 -17
  64. package/src/__tests__/secret-ingress-handler.test.ts +11 -0
  65. package/src/__tests__/secret-scanner.test.ts +43 -0
  66. package/src/__tests__/session-conflict-gate.test.ts +442 -6
  67. package/src/__tests__/session-init.benchmark.test.ts +3 -0
  68. package/src/__tests__/session-process-bridge.test.ts +242 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -1
  70. package/src/__tests__/shell-identity.test.ts +256 -0
  71. package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
  72. package/src/__tests__/subagent-tools.test.ts +637 -54
  73. package/src/__tests__/task-management-tools.test.ts +936 -0
  74. package/src/__tests__/task-runner.test.ts +2 -2
  75. package/src/__tests__/terminal-tools.test.ts +840 -0
  76. package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
  77. package/src/__tests__/tool-executor.test.ts +85 -151
  78. package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
  79. package/src/__tests__/trust-store.test.ts +28 -453
  80. package/src/__tests__/twilio-provider.test.ts +153 -3
  81. package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
  82. package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
  83. package/src/__tests__/twilio-routes.test.ts +17 -262
  84. package/src/__tests__/twitter-auth-handler.test.ts +2 -1
  85. package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
  86. package/src/__tests__/twitter-cli-routing.test.ts +252 -0
  87. package/src/__tests__/twitter-oauth-client.test.ts +209 -0
  88. package/src/__tests__/workspace-policy.test.ts +213 -0
  89. package/src/calls/call-bridge.ts +92 -19
  90. package/src/calls/call-domain.ts +157 -5
  91. package/src/calls/call-orchestrator.ts +96 -8
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +97 -0
  94. package/src/calls/elevenlabs-config.ts +31 -0
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +50 -6
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +114 -0
  99. package/src/cli/twitter.ts +200 -21
  100. package/src/cli.ts +1 -20
  101. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
  102. package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
  103. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
  104. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  105. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
  106. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  107. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
  108. package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
  109. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
  110. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
  111. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
  112. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
  113. package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
  114. package/src/config/bundled-skills/twitter/SKILL.md +103 -17
  115. package/src/config/defaults.ts +26 -2
  116. package/src/config/schema.ts +178 -9
  117. package/src/config/types.ts +3 -0
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
  119. package/src/daemon/assistant-attachments.ts +4 -2
  120. package/src/daemon/handlers/apps.ts +69 -0
  121. package/src/daemon/handlers/config.ts +543 -24
  122. package/src/daemon/handlers/index.ts +1 -0
  123. package/src/daemon/handlers/sessions.ts +22 -6
  124. package/src/daemon/handlers/shared.ts +2 -1
  125. package/src/daemon/handlers/skills.ts +5 -20
  126. package/src/daemon/ipc-contract-inventory.json +28 -0
  127. package/src/daemon/ipc-contract.ts +168 -10
  128. package/src/daemon/ipc-validate.ts +17 -0
  129. package/src/daemon/lifecycle.ts +2 -0
  130. package/src/daemon/server.ts +78 -72
  131. package/src/daemon/session-attachments.ts +1 -1
  132. package/src/daemon/session-conflict-gate.ts +62 -6
  133. package/src/daemon/session-notifiers.ts +1 -1
  134. package/src/daemon/session-process.ts +62 -3
  135. package/src/daemon/session-tool-setup.ts +1 -2
  136. package/src/daemon/tls-certs.ts +189 -0
  137. package/src/daemon/video-thumbnail.ts +5 -3
  138. package/src/hooks/manager.ts +5 -9
  139. package/src/memory/app-git-service.ts +295 -0
  140. package/src/memory/app-store.ts +21 -0
  141. package/src/memory/conflict-intent.ts +47 -4
  142. package/src/memory/conflict-policy.ts +73 -0
  143. package/src/memory/conflict-store.ts +9 -1
  144. package/src/memory/contradiction-checker.ts +28 -0
  145. package/src/memory/conversation-key-store.ts +15 -0
  146. package/src/memory/db.ts +81 -0
  147. package/src/memory/embedding-local.ts +3 -13
  148. package/src/memory/external-conversation-store.ts +234 -0
  149. package/src/memory/job-handlers/conflict.ts +22 -2
  150. package/src/memory/jobs-worker.ts +67 -28
  151. package/src/memory/runs-store.ts +54 -7
  152. package/src/memory/schema.ts +20 -0
  153. package/src/messaging/provider.ts +9 -0
  154. package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
  155. package/src/messaging/providers/telegram-bot/client.ts +104 -0
  156. package/src/messaging/providers/telegram-bot/types.ts +15 -0
  157. package/src/messaging/registry.ts +1 -0
  158. package/src/permissions/checker.ts +48 -44
  159. package/src/permissions/defaults.ts +11 -0
  160. package/src/permissions/prompter.ts +0 -4
  161. package/src/permissions/shell-identity.ts +227 -0
  162. package/src/permissions/trust-store.ts +76 -53
  163. package/src/permissions/types.ts +0 -19
  164. package/src/permissions/workspace-policy.ts +114 -0
  165. package/src/providers/retry.ts +12 -37
  166. package/src/runtime/assistant-event-hub.ts +41 -4
  167. package/src/runtime/channel-approval-parser.ts +60 -0
  168. package/src/runtime/channel-approval-types.ts +71 -0
  169. package/src/runtime/channel-approvals.ts +145 -0
  170. package/src/runtime/gateway-client.ts +16 -0
  171. package/src/runtime/http-server.ts +29 -9
  172. package/src/runtime/routes/call-routes.ts +52 -2
  173. package/src/runtime/routes/channel-routes.ts +296 -16
  174. package/src/runtime/routes/conversation-routes.ts +12 -5
  175. package/src/runtime/routes/events-routes.ts +97 -28
  176. package/src/runtime/routes/run-routes.ts +2 -7
  177. package/src/runtime/run-orchestrator.ts +0 -3
  178. package/src/schedule/recurrence-engine.ts +26 -2
  179. package/src/schedule/recurrence-types.ts +1 -1
  180. package/src/schedule/schedule-store.ts +12 -3
  181. package/src/security/secret-scanner.ts +7 -0
  182. package/src/tasks/ephemeral-permissions.ts +0 -2
  183. package/src/tasks/task-scheduler.ts +2 -1
  184. package/src/tools/calls/call-start.ts +8 -0
  185. package/src/tools/execution-target.ts +21 -0
  186. package/src/tools/execution-timeout.ts +49 -0
  187. package/src/tools/executor.ts +6 -135
  188. package/src/tools/network/web-search.ts +9 -32
  189. package/src/tools/policy-context.ts +29 -0
  190. package/src/tools/schedule/update.ts +8 -1
  191. package/src/tools/terminal/parser.ts +16 -18
  192. package/src/tools/types.ts +4 -11
  193. package/src/twitter/oauth-client.ts +102 -0
  194. package/src/twitter/router.ts +101 -0
  195. package/src/util/debounce.ts +88 -0
  196. package/src/util/network-info.ts +47 -0
  197. package/src/util/platform.ts +29 -4
  198. package/src/util/promise-guard.ts +37 -0
  199. package/src/util/retry.ts +98 -0
  200. package/src/util/truncate.ts +1 -1
  201. package/src/workspace/git-service.ts +129 -112
  202. package/src/tools/contacts/contact-merge.ts +0 -55
  203. package/src/tools/contacts/contact-search.ts +0 -58
  204. package/src/tools/contacts/contact-upsert.ts +0 -64
  205. package/src/tools/playbooks/index.ts +0 -4
  206. package/src/tools/playbooks/playbook-create.ts +0 -96
  207. package/src/tools/playbooks/playbook-delete.ts +0 -52
  208. package/src/tools/playbooks/playbook-list.ts +0 -74
  209. package/src/tools/playbooks/playbook-update.ts +0 -111
@@ -7,8 +7,19 @@ const VALID_SECRET_ACTIONS = ['redact', 'warn', 'block', 'prompt'] as const;
7
7
  const VALID_MEMORY_EMBEDDING_PROVIDERS = ['auto', 'local', 'openai', 'gemini', 'ollama'] as const;
8
8
  const VALID_SANDBOX_BACKENDS = ['native', 'docker'] as const;
9
9
  const VALID_DOCKER_NETWORKS = ['none', 'bridge'] as const;
10
- const VALID_PERMISSIONS_MODES = ['legacy', 'strict'] as const;
10
+ const VALID_PERMISSIONS_MODES = ['legacy', 'strict', 'workspace'] as const;
11
11
  const VALID_CALL_PROVIDERS = ['twilio'] as const;
12
+ const VALID_CALL_VOICE_MODES = ['twilio_standard', 'twilio_elevenlabs_tts', 'elevenlabs_agent'] as const;
13
+ export const VALID_CALLER_IDENTITY_MODES = ['assistant_number', 'user_number'] as const;
14
+ const VALID_CALL_TRANSCRIPTION_PROVIDERS = ['Deepgram', 'Google'] as const;
15
+ const VALID_MEMORY_ITEM_KINDS = [
16
+ 'preference', 'profile', 'project', 'decision', 'todo',
17
+ 'fact', 'constraint', 'relationship', 'event', 'opinion', 'instruction', 'style',
18
+ ] as const;
19
+
20
+ const DEFAULT_CONFLICTABLE_KINDS = [
21
+ 'preference', 'profile', 'constraint', 'instruction', 'style',
22
+ ] as const;
12
23
 
13
24
  export const TimeoutConfigSchema = z.object({
14
25
  shellMaxTimeoutSec: z
@@ -126,7 +137,7 @@ export const PermissionsConfigSchema = z.object({
126
137
  .enum(VALID_PERMISSIONS_MODES, {
127
138
  error: `permissions.mode must be one of: ${VALID_PERMISSIONS_MODES.join(', ')}`,
128
139
  })
129
- .default('strict'),
140
+ .default('workspace'),
130
141
  });
131
142
 
132
143
  export const AuditLogConfigSchema = z.object({
@@ -426,6 +437,11 @@ export const MemoryJobsConfigSchema = z.object({
426
437
  .int('memory.jobs.workerConcurrency must be an integer')
427
438
  .positive('memory.jobs.workerConcurrency must be a positive integer')
428
439
  .default(2),
440
+ batchSize: z
441
+ .number({ error: 'memory.jobs.batchSize must be a number' })
442
+ .int('memory.jobs.batchSize must be an integer')
443
+ .positive('memory.jobs.batchSize must be a positive integer')
444
+ .default(10),
429
445
  });
430
446
 
431
447
  export const MemoryRetentionConfigSchema = z.object({
@@ -548,6 +564,17 @@ export const MemoryConflictsConfigSchema = z.object({
548
564
  .min(0, 'memory.conflicts.relevanceThreshold must be >= 0')
549
565
  .max(1, 'memory.conflicts.relevanceThreshold must be <= 1')
550
566
  .default(0.3),
567
+ askOnIrrelevantTurns: z
568
+ .boolean({ error: 'memory.conflicts.askOnIrrelevantTurns must be a boolean' })
569
+ .default(false),
570
+ conflictableKinds: z
571
+ .array(
572
+ z.enum(VALID_MEMORY_ITEM_KINDS, {
573
+ error: `memory.conflicts.conflictableKinds entries must be one of: ${VALID_MEMORY_ITEM_KINDS.join(', ')}`,
574
+ }),
575
+ )
576
+ .nonempty({ message: 'memory.conflicts.conflictableKinds must not be empty' })
577
+ .default([...DEFAULT_CONFLICTABLE_KINDS]),
551
578
  });
552
579
 
553
580
  export const MemoryProfileConfigSchema = z.object({
@@ -626,6 +653,7 @@ export const MemoryConfigSchema = z.object({
626
653
  }),
627
654
  jobs: MemoryJobsConfigSchema.default({
628
655
  workerConcurrency: 2,
656
+ batchSize: 10,
629
657
  }),
630
658
  retention: MemoryRetentionConfigSchema.default({
631
659
  keepRawForever: true,
@@ -667,6 +695,8 @@ export const MemoryConfigSchema = z.object({
667
695
  reaskCooldownTurns: 3,
668
696
  resolverLlmTimeoutMs: 12000,
669
697
  relevanceThreshold: 0.3,
698
+ askOnIrrelevantTurns: false,
699
+ conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'],
670
700
  }),
671
701
  profile: MemoryProfileConfigSchema.default({
672
702
  enabled: true,
@@ -885,6 +915,84 @@ export const CallsSafetyConfigSchema = z.object({
885
915
  .default([]),
886
916
  });
887
917
 
918
+ export const CallsElevenLabsConfigSchema = z.object({
919
+ voiceId: z
920
+ .string({ error: 'calls.voice.elevenlabs.voiceId must be a string' })
921
+ .default(''),
922
+ voiceModelId: z
923
+ .string({ error: 'calls.voice.elevenlabs.voiceModelId must be a string' })
924
+ .default(''),
925
+ speed: z
926
+ .number({ error: 'calls.voice.elevenlabs.speed must be a number' })
927
+ .min(0.7, 'calls.voice.elevenlabs.speed must be >= 0.7')
928
+ .max(1.2, 'calls.voice.elevenlabs.speed must be <= 1.2')
929
+ .default(1.0),
930
+ stability: z
931
+ .number({ error: 'calls.voice.elevenlabs.stability must be a number' })
932
+ .min(0, 'calls.voice.elevenlabs.stability must be >= 0')
933
+ .max(1, 'calls.voice.elevenlabs.stability must be <= 1')
934
+ .default(0.5),
935
+ similarityBoost: z
936
+ .number({ error: 'calls.voice.elevenlabs.similarityBoost must be a number' })
937
+ .min(0, 'calls.voice.elevenlabs.similarityBoost must be >= 0')
938
+ .max(1, 'calls.voice.elevenlabs.similarityBoost must be <= 1')
939
+ .default(0.75),
940
+ useSpeakerBoost: z
941
+ .boolean({ error: 'calls.voice.elevenlabs.useSpeakerBoost must be a boolean' })
942
+ .default(true),
943
+ agentId: z
944
+ .string({ error: 'calls.voice.elevenlabs.agentId must be a string' })
945
+ .default(''),
946
+ apiBaseUrl: z
947
+ .string({ error: 'calls.voice.elevenlabs.apiBaseUrl must be a string' })
948
+ .default('https://api.elevenlabs.io'),
949
+ registerCallTimeoutMs: z
950
+ .number({ error: 'calls.voice.elevenlabs.registerCallTimeoutMs must be a number' })
951
+ .int('calls.voice.elevenlabs.registerCallTimeoutMs must be an integer')
952
+ .min(1000, 'calls.voice.elevenlabs.registerCallTimeoutMs must be >= 1000')
953
+ .max(15000, 'calls.voice.elevenlabs.registerCallTimeoutMs must be <= 15000')
954
+ .default(5000),
955
+ });
956
+
957
+ export const CallsVoiceConfigSchema = z.object({
958
+ mode: z
959
+ .enum(VALID_CALL_VOICE_MODES, {
960
+ error: `calls.voice.mode must be one of: ${VALID_CALL_VOICE_MODES.join(', ')}`,
961
+ })
962
+ .default('twilio_standard'),
963
+ language: z
964
+ .string({ error: 'calls.voice.language must be a string' })
965
+ .default('en-US'),
966
+ transcriptionProvider: z
967
+ .enum(VALID_CALL_TRANSCRIPTION_PROVIDERS, {
968
+ error: `calls.voice.transcriptionProvider must be one of: ${VALID_CALL_TRANSCRIPTION_PROVIDERS.join(', ')}`,
969
+ })
970
+ .default('Deepgram'),
971
+ fallbackToStandardOnError: z
972
+ .boolean({ error: 'calls.voice.fallbackToStandardOnError must be a boolean' })
973
+ .default(true),
974
+ elevenlabs: CallsElevenLabsConfigSchema.default({
975
+ voiceId: '',
976
+ voiceModelId: '',
977
+ speed: 1.0,
978
+ stability: 0.5,
979
+ similarityBoost: 0.75,
980
+ useSpeakerBoost: true,
981
+ agentId: '',
982
+ apiBaseUrl: 'https://api.elevenlabs.io',
983
+ registerCallTimeoutMs: 5000,
984
+ }),
985
+ });
986
+
987
+ export const CallerIdentityConfigSchema = z.object({
988
+ allowPerCallOverride: z
989
+ .boolean({ error: 'calls.callerIdentity.allowPerCallOverride must be a boolean' })
990
+ .default(true),
991
+ userNumber: z
992
+ .string({ error: 'calls.callerIdentity.userNumber must be a string' })
993
+ .optional(),
994
+ });
995
+
888
996
  export const CallsConfigSchema = z.object({
889
997
  enabled: z
890
998
  .boolean({ error: 'calls.enabled must be a boolean' })
@@ -913,6 +1021,29 @@ export const CallsConfigSchema = z.object({
913
1021
  safety: CallsSafetyConfigSchema.default({
914
1022
  denyCategories: [],
915
1023
  }),
1024
+ voice: CallsVoiceConfigSchema.default({
1025
+ mode: 'twilio_standard',
1026
+ language: 'en-US',
1027
+ transcriptionProvider: 'Deepgram',
1028
+ fallbackToStandardOnError: true,
1029
+ elevenlabs: {
1030
+ voiceId: '',
1031
+ voiceModelId: '',
1032
+ speed: 1.0,
1033
+ stability: 0.5,
1034
+ similarityBoost: 0.75,
1035
+ useSpeakerBoost: true,
1036
+ agentId: '',
1037
+ apiBaseUrl: 'https://api.elevenlabs.io',
1038
+ registerCallTimeoutMs: 5000,
1039
+ },
1040
+ }),
1041
+ model: z
1042
+ .string({ error: 'calls.model must be a string' })
1043
+ .optional(),
1044
+ callerIdentity: CallerIdentityConfigSchema.default({
1045
+ allowPerCallOverride: true,
1046
+ }),
916
1047
  });
917
1048
 
918
1049
  export const SkillsConfigSchema = z.object({
@@ -922,15 +1053,30 @@ export const SkillsConfigSchema = z.object({
922
1053
  allowBundled: z.array(z.string()).nullable().default(null),
923
1054
  });
924
1055
 
925
- export const IngressConfigSchema = z.object({
1056
+ const IngressBaseSchema = z.object({
926
1057
  enabled: z
927
1058
  .boolean({ error: 'ingress.enabled must be a boolean' })
928
- .default(false),
1059
+ .optional(),
929
1060
  publicBaseUrl: z
930
1061
  .string({ error: 'ingress.publicBaseUrl must be a string' })
931
1062
  .default(''),
932
1063
  });
933
1064
 
1065
+ export const IngressConfigSchema = IngressBaseSchema
1066
+ .default({ publicBaseUrl: '' })
1067
+ .transform((val) => ({
1068
+ ...val,
1069
+ // Backward compatibility: if `enabled` was never explicitly set (undefined),
1070
+ // infer it from whether a publicBaseUrl is configured. Existing users who
1071
+ // have a URL but predate the `enabled` field should not have their webhooks
1072
+ // silently disabled on upgrade.
1073
+ //
1074
+ // When publicBaseUrl is empty and enabled is unset, leave enabled as
1075
+ // undefined so getPublicBaseUrl() can still fall through to the
1076
+ // INGRESS_PUBLIC_BASE_URL env-var fallback (env-only setups).
1077
+ enabled: val.enabled ?? (val.publicBaseUrl ? true : undefined),
1078
+ }));
1079
+
934
1080
  export const AssistantConfigSchema = z.object({
935
1081
  provider: z
936
1082
  .enum(VALID_PROVIDERS, {
@@ -1028,6 +1174,7 @@ export const AssistantConfigSchema = z.object({
1028
1174
  },
1029
1175
  jobs: {
1030
1176
  workerConcurrency: 2,
1177
+ batchSize: 10,
1031
1178
  },
1032
1179
  retention: {
1033
1180
  keepRawForever: true,
@@ -1069,6 +1216,8 @@ export const AssistantConfigSchema = z.object({
1069
1216
  reaskCooldownTurns: 3,
1070
1217
  resolverLlmTimeoutMs: 12000,
1071
1218
  relevanceThreshold: 0.3,
1219
+ askOnIrrelevantTurns: false,
1220
+ conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'],
1072
1221
  },
1073
1222
  profile: {
1074
1223
  enabled: true,
@@ -1109,7 +1258,7 @@ export const AssistantConfigSchema = z.object({
1109
1258
  blockIngress: true,
1110
1259
  }),
1111
1260
  permissions: PermissionsConfigSchema.default({
1112
- mode: 'strict',
1261
+ mode: 'workspace',
1113
1262
  }),
1114
1263
  auditLog: AuditLogConfigSchema.default({
1115
1264
  retentionDays: 0,
@@ -1178,11 +1327,28 @@ export const AssistantConfigSchema = z.object({
1178
1327
  safety: {
1179
1328
  denyCategories: [],
1180
1329
  },
1330
+ voice: {
1331
+ mode: 'twilio_standard',
1332
+ language: 'en-US',
1333
+ transcriptionProvider: 'Deepgram',
1334
+ fallbackToStandardOnError: true,
1335
+ elevenlabs: {
1336
+ voiceId: '',
1337
+ voiceModelId: '',
1338
+ speed: 1.0,
1339
+ stability: 0.5,
1340
+ similarityBoost: 0.75,
1341
+ useSpeakerBoost: true,
1342
+ agentId: '',
1343
+ apiBaseUrl: 'https://api.elevenlabs.io',
1344
+ registerCallTimeoutMs: 5000,
1345
+ },
1346
+ },
1347
+ callerIdentity: {
1348
+ allowPerCallOverride: true,
1349
+ },
1181
1350
  }),
1182
- ingress: IngressConfigSchema.default({
1183
- enabled: false,
1184
- publicBaseUrl: '',
1185
- }),
1351
+ ingress: IngressConfigSchema,
1186
1352
  }).superRefine((config, ctx) => {
1187
1353
  if (config.contextWindow.targetInputTokens >= config.contextWindow.maxInputTokens) {
1188
1354
  ctx.addIssue({
@@ -1243,4 +1409,7 @@ export type WorkspaceGitConfig = z.infer<typeof WorkspaceGitConfigSchema>;
1243
1409
  export type CallsConfig = z.infer<typeof CallsConfigSchema>;
1244
1410
  export type CallsDisclosureConfig = z.infer<typeof CallsDisclosureConfigSchema>;
1245
1411
  export type CallsSafetyConfig = z.infer<typeof CallsSafetyConfigSchema>;
1412
+ export type CallsVoiceConfig = z.infer<typeof CallsVoiceConfigSchema>;
1413
+ export type CallsElevenLabsConfig = z.infer<typeof CallsElevenLabsConfigSchema>;
1414
+ export type CallerIdentityConfig = z.infer<typeof CallerIdentityConfigSchema>;
1246
1415
  export type IngressConfig = z.infer<typeof IngressConfigSchema>;
@@ -34,5 +34,8 @@ export type {
34
34
  CallsConfig,
35
35
  CallsDisclosureConfig,
36
36
  CallsSafetyConfig,
37
+ CallsVoiceConfig,
38
+ CallsElevenLabsConfig,
39
+ CallerIdentityConfig,
37
40
  IngressConfig,
38
41
  } from './schema.js';
@@ -13,90 +13,85 @@ You are helping your user connect a Telegram bot to the Vellum Assistant gateway
13
13
  1. **Bot token** from Telegram's @BotFather (the user provides this)
14
14
  2. **Gateway webhook URL** — derived from the canonical ingress setting: `${ingress.publicBaseUrl}/webhooks/telegram`. The gateway is the only publicly reachable endpoint; Telegram sends webhooks to the gateway, which validates and forwards them to the assistant runtime internally. If `ingress.publicBaseUrl` is not configured, load and execute the **public-ingress** skill first (`skill_load` with `skill: "public-ingress"`) to set up an ngrok tunnel and persist the URL before continuing.
15
15
 
16
- If the user has already provided the bot token in the conversation, use it directly. Otherwise, ask for it.
16
+ **IMPORTANT — Secure credential collection only:** Never use a bot token that was pasted in plaintext chat. Always collect the bot token through the secure credential prompt flow using `credential_store` with `action: "prompt"` and `service: "telegram"`, `field: "bot_token"`. If the user has already pasted a token in the conversation, inform them that for security reasons you cannot use tokens shared in chat and must collect it through the secure prompt instead.
17
17
 
18
18
  ## Setup Steps
19
19
 
20
- ### Step 1: Verify the Bot Token
20
+ ### Step 1: Collect the Bot Token Securely
21
21
 
22
- Use `evaluate_typescript_code` to call the Telegram `getMe` API and confirm the token is valid:
22
+ Collect the bot token through the secure credential prompt:
23
+ - Call `credential_store` with `action: "prompt"`, `service: "telegram"`, `field: "bot_token"`, `label: "Telegram Bot Token"`, `description: "Enter the bot token you received from @BotFather"`, and `placeholder: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"`.
23
24
 
24
- ```typescript
25
- export default async (input: { token: string }) => {
26
- const res = await fetch(`https://api.telegram.org/bot${input.token}/getMe`, { method: 'POST' });
27
- return res.json();
28
- };
29
- ```
25
+ The token is collected securely via a system-level prompt and is never exposed in plaintext chat.
30
26
 
31
- Pass the bot token via `mock_input_json`. Verify the response has `ok: true` and note the bot's username and ID.
27
+ ### Step 2: Configure via Daemon
32
28
 
33
- If the token is invalid, tell the user and ask them to double-check it.
29
+ After the token is collected, send it to the daemon's `telegram_config` handler which validates, stores, and configures everything in one step:
34
30
 
35
- ### Step 2: Generate a Webhook Secret
31
+ - Send the `telegram_config` IPC message with `action: "set"`. The daemon retrieves the token from secure storage internally when `botToken` is not provided in the message — you do not need to retrieve it yourself.
36
32
 
37
- Use `evaluate_typescript_code` to generate a random secret:
33
+ The daemon's `telegram_config set` handler automatically:
34
+ - Validates the token by calling the Telegram `getMe` API
35
+ - Stores the bot token in secure storage with bot username metadata
36
+ - Generates a webhook secret if one does not already exist
37
+ - Triggers an immediate gateway webhook reconcile
38
38
 
39
- ```typescript
40
- import { randomUUID } from 'node:crypto';
41
- export default () => ({ secret: randomUUID() });
42
- ```
39
+ If the token is invalid, the daemon returns an error. Tell the user and ask them to re-enter the token via the secure prompt.
43
40
 
44
- Save this value for the next steps.
41
+ ### Step 3: Webhook Registration (Automatic)
45
42
 
46
- ### Step 3: Register the Webhook
43
+ Manual webhook registration is no longer required. The gateway automatically reconciles the Telegram webhook on startup and whenever credentials change. It compares the current webhook URL against `${INGRESS_PUBLIC_BASE_URL}/webhooks/telegram` and updates it if needed, including the webhook secret and allowed updates.
47
44
 
48
- Use `evaluate_typescript_code` to register the webhook with Telegram:
49
-
50
- ```typescript
51
- export default async (input: { token: string; url: string; secret: string }) => {
52
- const res = await fetch(`https://api.telegram.org/bot${input.token}/setWebhook`, {
53
- method: 'POST',
54
- headers: { 'Content-Type': 'application/json' },
55
- body: JSON.stringify({
56
- url: input.url,
57
- secret_token: input.secret,
58
- allowed_updates: ['message', 'edited_message'],
59
- }),
60
- });
61
- return res.json();
62
- };
63
- ```
64
-
65
- Verify the response has `ok: true`.
45
+ If the webhook secret changes (e.g., secret rotation), the gateway's credential watcher detects the change and re-registers the webhook automatically. If the ingress URL changes (e.g., tunnel restart), the assistant daemon triggers an immediate internal reconcile so the webhook re-registers automatically without a gateway restart.
66
46
 
67
47
  ### Step 4: Register Bot Commands
68
48
 
69
- Use `evaluate_typescript_code` to register the `/new` command:
70
-
71
- ```typescript
72
- export default async (input: { token: string }) => {
73
- const res = await fetch(`https://api.telegram.org/bot${input.token}/setMyCommands`, {
74
- method: 'POST',
75
- headers: { 'Content-Type': 'application/json' },
76
- body: JSON.stringify({
77
- commands: [{ command: 'new', description: 'Start a new conversation' }],
78
- }),
79
- });
80
- return res.json();
81
- };
82
- ```
49
+ Send the `telegram_config` IPC message with `action: "set_commands"` to register the `/new` command. The daemon handles token retrieval from secure storage internally — you do not need to retrieve it yourself.
83
50
 
84
- ### Step 5: Store Credentials
51
+ ### Step 5: Validate Routing Configuration
85
52
 
86
- Use `credential_store` twice to securely save the credentials:
53
+ Verify that the gateway routing is configured to deliver inbound messages to the assistant:
87
54
 
88
- 1. **Store the bot token:**
89
- - action: `store`, service: `telegram`, field: `bot_token`, value: the bot token
55
+ - In **single-assistant mode** (the default local deployment), routing is automatically configured. The CLI sets `GATEWAY_UNMAPPED_POLICY=default` and `GATEWAY_DEFAULT_ASSISTANT_ID` to the current assistant's ID when starting the gateway, so no manual routing configuration is needed.
56
+ - In **multi-assistant mode**, the operator must set `GATEWAY_ASSISTANT_ROUTING_JSON` to map specific chat IDs or user IDs to assistant IDs, or configure a default assistant via `GATEWAY_DEFAULT_ASSISTANT_ID` with `GATEWAY_UNMAPPED_POLICY=default`.
90
57
 
91
- 2. **Store the webhook secret:**
92
- - action: `store`, service: `telegram`, field: `webhook_secret`, value: the generated secret
58
+ If routing is misconfigured, inbound Telegram messages will be rejected and the gateway will send a visible notice to the chat explaining the issue (rate-limited to once per 5 minutes per chat).
93
59
 
94
60
  ### Step 6: Report Success
95
61
 
96
62
  Summarize what was done:
97
- - Bot verified: @username (ID: nnn)
98
- - Webhook registered at the provided URL
63
+ - Bot verified and credentials stored securely via daemon
64
+ - Webhook registration: handled automatically by the gateway
99
65
  - Bot commands registered: /new
100
- - Credentials stored securely in the vault
66
+ - Routing configuration validated
67
+
68
+ The gateway automatically detects credentials from the vault, reconciles the Telegram webhook registration, and begins accepting Telegram webhooks shortly. In single-assistant mode, routing is automatically configured — no manual environment variable configuration or webhook registration is needed. If the webhook secret changes later, the gateway's credential watcher will automatically re-register the webhook. If the ingress URL changes (e.g., tunnel restart), the assistant daemon triggers an immediate internal reconcile so the webhook re-registers automatically without a gateway restart.
69
+
70
+ ## Bot-Account Limitations
71
+
72
+ Telegram bot accounts have inherent limitations imposed by the Bot API:
73
+
74
+ - **No arbitrary messaging**: Bots cannot initiate conversations with users who have not first interacted with the bot (sent `/start` or added it to a group). Messaging arbitrary phone numbers is not possible.
75
+ - **No conversation listing**: The Bot API does not expose a method to enumerate the chats a bot belongs to.
76
+ - **No message history retrieval**: Bots cannot fetch past messages from a chat.
77
+ - **No message search**: No search API is available for bots.
78
+
79
+ These limitations apply to all Telegram bots regardless of configuration. Future support for MTProto user-account sessions may lift some of these restrictions.
80
+
81
+ ## Automated vs Manual Steps
82
+
83
+ The following steps are now **automated** by the gateway and CLI:
84
+
85
+ | Step | Status | Details |
86
+ |------|--------|---------|
87
+ | Webhook registration | Automated | The gateway reconciles the webhook URL on startup and when credentials change |
88
+ | Routing configuration | Automated (single-assistant) | The CLI sets `GATEWAY_UNMAPPED_POLICY=default` and `GATEWAY_DEFAULT_ASSISTANT_ID` automatically |
89
+ | Credential detection | Automated | The gateway watches the credential vault for changes |
90
+
91
+ The following steps still require **manual** action:
101
92
 
102
- The gateway automatically detects credentials from the vault and will begin accepting Telegram webhooks shortly. No manual environment variable configuration is needed.
93
+ | Step | Details |
94
+ |------|---------|
95
+ | Bot token from @BotFather | User must create a bot and provide the token via secure prompt |
96
+ | Bot command registration | Registered via the setup skill (Step 4 above) |
97
+ | Multi-assistant routing | Requires manual `GATEWAY_ASSISTANT_ROUTING_JSON` configuration |
@@ -320,8 +320,10 @@ export function drainDirectiveDisplayBuffer(buffer: string): DirectiveDisplayDra
320
320
  // streaming mode more data may arrive in the next chunk — eagerly
321
321
  // trimming would merge words across the directive boundary.
322
322
  const nextChar = buffer[end + 2];
323
- if (emitText.endsWith('\n') && (nextChar === '\n' || nextChar === '\r')) {
324
- emitText = emitText.slice(0, -1);
323
+ if (emitText.endsWith('\r\n') && nextChar === '\r') {
324
+ emitText = emitText.slice(0, -2); // trim full \r\n
325
+ } else if (emitText.endsWith('\n') && (nextChar === '\n' || nextChar === '\r')) {
326
+ emitText = emitText.slice(0, -1); // trim \n
325
327
  }
326
328
  }
327
329
 
@@ -16,8 +16,13 @@ import type {
16
16
  ShareAppCloudRequest,
17
17
  GalleryInstallRequest,
18
18
  AppUpdatePreviewRequest,
19
+ AppHistoryRequest,
20
+ AppDiffRequest,
21
+ AppFileAtVersionRequest,
22
+ AppRestoreRequest,
19
23
  UiSurfaceShow,
20
24
  } from '../ipc-protocol.js';
25
+ import { getAppHistory, getAppDiff, getAppFileAtVersion, restoreAppVersion } from '../../memory/app-git-service.js';
21
26
  import { log, compareSemver, createSigningCallback, defineHandlers, type HandlerContext } from './shared.js';
22
27
 
23
28
  export function handleAppDataRequest(
@@ -445,6 +450,66 @@ export function handleGalleryInstall(
445
450
  }
446
451
  }
447
452
 
453
+ export async function handleAppHistory(
454
+ msg: AppHistoryRequest,
455
+ socket: net.Socket,
456
+ ctx: HandlerContext,
457
+ ): Promise<void> {
458
+ try {
459
+ const versions = await getAppHistory(msg.appId, msg.limit);
460
+ ctx.send(socket, { type: 'app_history_response', appId: msg.appId, versions });
461
+ } catch (err) {
462
+ const message = err instanceof Error ? err.message : String(err);
463
+ log.error({ err, appId: msg.appId }, 'Failed to get app history');
464
+ ctx.send(socket, { type: 'error', message: `Failed to get app history: ${message}` });
465
+ }
466
+ }
467
+
468
+ export async function handleAppDiff(
469
+ msg: AppDiffRequest,
470
+ socket: net.Socket,
471
+ ctx: HandlerContext,
472
+ ): Promise<void> {
473
+ try {
474
+ const diff = await getAppDiff(msg.appId, msg.fromCommit, msg.toCommit);
475
+ ctx.send(socket, { type: 'app_diff_response', appId: msg.appId, diff });
476
+ } catch (err) {
477
+ const message = err instanceof Error ? err.message : String(err);
478
+ log.error({ err, appId: msg.appId }, 'Failed to get app diff');
479
+ ctx.send(socket, { type: 'error', message: `Failed to get app diff: ${message}` });
480
+ }
481
+ }
482
+
483
+ export async function handleAppFileAtVersion(
484
+ msg: AppFileAtVersionRequest,
485
+ socket: net.Socket,
486
+ ctx: HandlerContext,
487
+ ): Promise<void> {
488
+ try {
489
+ const content = await getAppFileAtVersion(msg.appId, msg.path, msg.commitHash);
490
+ ctx.send(socket, { type: 'app_file_at_version_response', appId: msg.appId, path: msg.path, content });
491
+ } catch (err) {
492
+ const message = err instanceof Error ? err.message : String(err);
493
+ log.error({ err, appId: msg.appId }, 'Failed to get app file at version');
494
+ ctx.send(socket, { type: 'error', message: `Failed to get app file at version: ${message}` });
495
+ }
496
+ }
497
+
498
+ export async function handleAppRestore(
499
+ msg: AppRestoreRequest,
500
+ socket: net.Socket,
501
+ ctx: HandlerContext,
502
+ ): Promise<void> {
503
+ try {
504
+ await restoreAppVersion(msg.appId, msg.commitHash);
505
+ ctx.send(socket, { type: 'app_restore_response', success: true });
506
+ } catch (err) {
507
+ const message = err instanceof Error ? err.message : String(err);
508
+ log.error({ err, appId: msg.appId }, 'Failed to restore app version');
509
+ ctx.send(socket, { type: 'app_restore_response', success: false, error: message });
510
+ }
511
+ }
512
+
448
513
  export const appHandlers = defineHandlers({
449
514
  app_data_request: handleAppDataRequest,
450
515
  app_open_request: handleAppOpenRequest,
@@ -458,4 +523,8 @@ export const appHandlers = defineHandlers({
458
523
  bundle_app: handleBundleApp,
459
524
  gallery_list: (_msg, socket, ctx) => handleGalleryList(socket, ctx),
460
525
  gallery_install: handleGalleryInstall,
526
+ app_history_request: handleAppHistory,
527
+ app_diff_request: handleAppDiff,
528
+ app_file_at_version_request: handleAppFileAtVersion,
529
+ app_restore_request: handleAppRestore,
461
530
  });