vellum 0.2.13 → 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 (207) 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 +113 -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 +137 -18
  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 +62 -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 +27 -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 +4 -4
  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 +93 -7
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +8 -0
  94. package/src/calls/elevenlabs-config.ts +7 -5
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +32 -37
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +29 -7
  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 +142 -34
  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 +10 -4
  116. package/src/config/schema.ts +80 -21
  117. package/src/config/types.ts +1 -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/prompter.ts +0 -4
  160. package/src/permissions/shell-identity.ts +227 -0
  161. package/src/permissions/trust-store.ts +76 -53
  162. package/src/permissions/types.ts +0 -19
  163. package/src/permissions/workspace-policy.ts +114 -0
  164. package/src/providers/retry.ts +12 -37
  165. package/src/runtime/assistant-event-hub.ts +41 -4
  166. package/src/runtime/channel-approval-parser.ts +60 -0
  167. package/src/runtime/channel-approval-types.ts +71 -0
  168. package/src/runtime/channel-approvals.ts +145 -0
  169. package/src/runtime/gateway-client.ts +16 -0
  170. package/src/runtime/http-server.ts +29 -9
  171. package/src/runtime/routes/call-routes.ts +52 -2
  172. package/src/runtime/routes/channel-routes.ts +296 -16
  173. package/src/runtime/routes/events-routes.ts +97 -28
  174. package/src/runtime/routes/run-routes.ts +2 -7
  175. package/src/runtime/run-orchestrator.ts +0 -3
  176. package/src/schedule/recurrence-engine.ts +26 -2
  177. package/src/schedule/recurrence-types.ts +1 -1
  178. package/src/schedule/schedule-store.ts +12 -3
  179. package/src/security/secret-scanner.ts +7 -0
  180. package/src/tasks/ephemeral-permissions.ts +0 -2
  181. package/src/tasks/task-scheduler.ts +2 -1
  182. package/src/tools/calls/call-start.ts +8 -0
  183. package/src/tools/execution-target.ts +21 -0
  184. package/src/tools/execution-timeout.ts +49 -0
  185. package/src/tools/executor.ts +6 -135
  186. package/src/tools/network/web-search.ts +9 -32
  187. package/src/tools/policy-context.ts +29 -0
  188. package/src/tools/schedule/update.ts +8 -1
  189. package/src/tools/terminal/parser.ts +16 -18
  190. package/src/tools/types.ts +4 -11
  191. package/src/twitter/oauth-client.ts +102 -0
  192. package/src/twitter/router.ts +101 -0
  193. package/src/util/debounce.ts +88 -0
  194. package/src/util/network-info.ts +47 -0
  195. package/src/util/platform.ts +29 -4
  196. package/src/util/promise-guard.ts +37 -0
  197. package/src/util/retry.ts +98 -0
  198. package/src/util/truncate.ts +1 -1
  199. package/src/workspace/git-service.ts +129 -112
  200. package/src/tools/contacts/contact-merge.ts +0 -55
  201. package/src/tools/contacts/contact-search.ts +0 -58
  202. package/src/tools/contacts/contact-upsert.ts +0 -64
  203. package/src/tools/playbooks/index.ts +0 -4
  204. package/src/tools/playbooks/playbook-create.ts +0 -96
  205. package/src/tools/playbooks/playbook-delete.ts +0 -52
  206. package/src/tools/playbooks/playbook-list.ts +0 -74
  207. package/src/tools/playbooks/playbook-update.ts +0 -111
@@ -7,10 +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
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;
13
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;
14
23
 
15
24
  export const TimeoutConfigSchema = z.object({
16
25
  shellMaxTimeoutSec: z
@@ -128,7 +137,7 @@ export const PermissionsConfigSchema = z.object({
128
137
  .enum(VALID_PERMISSIONS_MODES, {
129
138
  error: `permissions.mode must be one of: ${VALID_PERMISSIONS_MODES.join(', ')}`,
130
139
  })
131
- .default('strict'),
140
+ .default('workspace'),
132
141
  });
133
142
 
134
143
  export const AuditLogConfigSchema = z.object({
@@ -428,6 +437,11 @@ export const MemoryJobsConfigSchema = z.object({
428
437
  .int('memory.jobs.workerConcurrency must be an integer')
429
438
  .positive('memory.jobs.workerConcurrency must be a positive integer')
430
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),
431
445
  });
432
446
 
433
447
  export const MemoryRetentionConfigSchema = z.object({
@@ -550,6 +564,17 @@ export const MemoryConflictsConfigSchema = z.object({
550
564
  .min(0, 'memory.conflicts.relevanceThreshold must be >= 0')
551
565
  .max(1, 'memory.conflicts.relevanceThreshold must be <= 1')
552
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]),
553
578
  });
554
579
 
555
580
  export const MemoryProfileConfigSchema = z.object({
@@ -628,6 +653,7 @@ export const MemoryConfigSchema = z.object({
628
653
  }),
629
654
  jobs: MemoryJobsConfigSchema.default({
630
655
  workerConcurrency: 2,
656
+ batchSize: 10,
631
657
  }),
632
658
  retention: MemoryRetentionConfigSchema.default({
633
659
  keepRawForever: true,
@@ -669,6 +695,8 @@ export const MemoryConfigSchema = z.object({
669
695
  reaskCooldownTurns: 3,
670
696
  resolverLlmTimeoutMs: 12000,
671
697
  relevanceThreshold: 0.3,
698
+ askOnIrrelevantTurns: false,
699
+ conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'],
672
700
  }),
673
701
  profile: MemoryProfileConfigSchema.default({
674
702
  enabled: true,
@@ -893,7 +921,12 @@ export const CallsElevenLabsConfigSchema = z.object({
893
921
  .default(''),
894
922
  voiceModelId: z
895
923
  .string({ error: 'calls.voice.elevenlabs.voiceModelId must be a string' })
896
- .default('turbo_v2_5'),
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),
897
930
  stability: z
898
931
  .number({ error: 'calls.voice.elevenlabs.stability must be a number' })
899
932
  .min(0, 'calls.voice.elevenlabs.stability must be >= 0')
@@ -904,11 +937,6 @@ export const CallsElevenLabsConfigSchema = z.object({
904
937
  .min(0, 'calls.voice.elevenlabs.similarityBoost must be >= 0')
905
938
  .max(1, 'calls.voice.elevenlabs.similarityBoost must be <= 1')
906
939
  .default(0.75),
907
- style: z
908
- .number({ error: 'calls.voice.elevenlabs.style must be a number' })
909
- .min(0, 'calls.voice.elevenlabs.style must be >= 0')
910
- .max(1, 'calls.voice.elevenlabs.style must be <= 1')
911
- .default(0.0),
912
940
  useSpeakerBoost: z
913
941
  .boolean({ error: 'calls.voice.elevenlabs.useSpeakerBoost must be a boolean' })
914
942
  .default(true),
@@ -945,10 +973,10 @@ export const CallsVoiceConfigSchema = z.object({
945
973
  .default(true),
946
974
  elevenlabs: CallsElevenLabsConfigSchema.default({
947
975
  voiceId: '',
948
- voiceModelId: 'turbo_v2_5',
976
+ voiceModelId: '',
977
+ speed: 1.0,
949
978
  stability: 0.5,
950
979
  similarityBoost: 0.75,
951
- style: 0.0,
952
980
  useSpeakerBoost: true,
953
981
  agentId: '',
954
982
  apiBaseUrl: 'https://api.elevenlabs.io',
@@ -956,6 +984,15 @@ export const CallsVoiceConfigSchema = z.object({
956
984
  }),
957
985
  });
958
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
+
959
996
  export const CallsConfigSchema = z.object({
960
997
  enabled: z
961
998
  .boolean({ error: 'calls.enabled must be a boolean' })
@@ -991,10 +1028,10 @@ export const CallsConfigSchema = z.object({
991
1028
  fallbackToStandardOnError: true,
992
1029
  elevenlabs: {
993
1030
  voiceId: '',
994
- voiceModelId: 'turbo_v2_5',
1031
+ voiceModelId: '',
1032
+ speed: 1.0,
995
1033
  stability: 0.5,
996
1034
  similarityBoost: 0.75,
997
- style: 0.0,
998
1035
  useSpeakerBoost: true,
999
1036
  agentId: '',
1000
1037
  apiBaseUrl: 'https://api.elevenlabs.io',
@@ -1004,6 +1041,9 @@ export const CallsConfigSchema = z.object({
1004
1041
  model: z
1005
1042
  .string({ error: 'calls.model must be a string' })
1006
1043
  .optional(),
1044
+ callerIdentity: CallerIdentityConfigSchema.default({
1045
+ allowPerCallOverride: true,
1046
+ }),
1007
1047
  });
1008
1048
 
1009
1049
  export const SkillsConfigSchema = z.object({
@@ -1013,15 +1053,30 @@ export const SkillsConfigSchema = z.object({
1013
1053
  allowBundled: z.array(z.string()).nullable().default(null),
1014
1054
  });
1015
1055
 
1016
- export const IngressConfigSchema = z.object({
1056
+ const IngressBaseSchema = z.object({
1017
1057
  enabled: z
1018
1058
  .boolean({ error: 'ingress.enabled must be a boolean' })
1019
- .default(false),
1059
+ .optional(),
1020
1060
  publicBaseUrl: z
1021
1061
  .string({ error: 'ingress.publicBaseUrl must be a string' })
1022
1062
  .default(''),
1023
1063
  });
1024
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
+
1025
1080
  export const AssistantConfigSchema = z.object({
1026
1081
  provider: z
1027
1082
  .enum(VALID_PROVIDERS, {
@@ -1119,6 +1174,7 @@ export const AssistantConfigSchema = z.object({
1119
1174
  },
1120
1175
  jobs: {
1121
1176
  workerConcurrency: 2,
1177
+ batchSize: 10,
1122
1178
  },
1123
1179
  retention: {
1124
1180
  keepRawForever: true,
@@ -1160,6 +1216,8 @@ export const AssistantConfigSchema = z.object({
1160
1216
  reaskCooldownTurns: 3,
1161
1217
  resolverLlmTimeoutMs: 12000,
1162
1218
  relevanceThreshold: 0.3,
1219
+ askOnIrrelevantTurns: false,
1220
+ conflictableKinds: ['preference', 'profile', 'constraint', 'instruction', 'style'],
1163
1221
  },
1164
1222
  profile: {
1165
1223
  enabled: true,
@@ -1200,7 +1258,7 @@ export const AssistantConfigSchema = z.object({
1200
1258
  blockIngress: true,
1201
1259
  }),
1202
1260
  permissions: PermissionsConfigSchema.default({
1203
- mode: 'strict',
1261
+ mode: 'workspace',
1204
1262
  }),
1205
1263
  auditLog: AuditLogConfigSchema.default({
1206
1264
  retentionDays: 0,
@@ -1276,21 +1334,21 @@ export const AssistantConfigSchema = z.object({
1276
1334
  fallbackToStandardOnError: true,
1277
1335
  elevenlabs: {
1278
1336
  voiceId: '',
1279
- voiceModelId: 'turbo_v2_5',
1337
+ voiceModelId: '',
1338
+ speed: 1.0,
1280
1339
  stability: 0.5,
1281
1340
  similarityBoost: 0.75,
1282
- style: 0.0,
1283
1341
  useSpeakerBoost: true,
1284
1342
  agentId: '',
1285
1343
  apiBaseUrl: 'https://api.elevenlabs.io',
1286
1344
  registerCallTimeoutMs: 5000,
1287
1345
  },
1288
1346
  },
1347
+ callerIdentity: {
1348
+ allowPerCallOverride: true,
1349
+ },
1289
1350
  }),
1290
- ingress: IngressConfigSchema.default({
1291
- enabled: false,
1292
- publicBaseUrl: '',
1293
- }),
1351
+ ingress: IngressConfigSchema,
1294
1352
  }).superRefine((config, ctx) => {
1295
1353
  if (config.contextWindow.targetInputTokens >= config.contextWindow.maxInputTokens) {
1296
1354
  ctx.addIssue({
@@ -1353,4 +1411,5 @@ export type CallsDisclosureConfig = z.infer<typeof CallsDisclosureConfigSchema>;
1353
1411
  export type CallsSafetyConfig = z.infer<typeof CallsSafetyConfigSchema>;
1354
1412
  export type CallsVoiceConfig = z.infer<typeof CallsVoiceConfigSchema>;
1355
1413
  export type CallsElevenLabsConfig = z.infer<typeof CallsElevenLabsConfigSchema>;
1414
+ export type CallerIdentityConfig = z.infer<typeof CallerIdentityConfigSchema>;
1356
1415
  export type IngressConfig = z.infer<typeof IngressConfigSchema>;
@@ -36,5 +36,6 @@ export type {
36
36
  CallsSafetyConfig,
37
37
  CallsVoiceConfig,
38
38
  CallsElevenLabsConfig,
39
+ CallerIdentityConfig,
39
40
  IngressConfig,
40
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
  });