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
@@ -8,64 +8,133 @@
8
8
  */
9
9
 
10
10
  import { getOrCreateConversation } from '../../memory/conversation-key-store.js';
11
- import { assistantEventHub } from '../assistant-event-hub.js';
12
- import { formatSseFrame } from '../assistant-event.js';
11
+ import { assistantEventHub, AssistantEventHub } from '../assistant-event-hub.js';
12
+ import { formatSseFrame, formatSseHeartbeat } from '../assistant-event.js';
13
13
  import type { AssistantEventSubscription } from '../assistant-event-hub.js';
14
14
 
15
+ /** Keep-alive comment sent to idle clients every 30 s by default. */
16
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
17
+
15
18
  /**
16
19
  * Stream assistant events as Server-Sent Events for a specific conversation.
17
20
  *
18
21
  * Query params:
19
22
  * conversationKey — required; scopes the stream to one conversation.
23
+ *
24
+ * Options (for testing):
25
+ * hub — override the event hub (defaults to process singleton).
26
+ * heartbeatIntervalMs — how often to emit keep-alive comments (default 30 s).
20
27
  */
21
28
  export function handleSubscribeAssistantEvents(
22
29
  req: Request,
23
30
  url: URL,
31
+ options?: {
32
+ hub?: AssistantEventHub;
33
+ heartbeatIntervalMs?: number;
34
+ },
24
35
  ): Response {
25
36
  const conversationKey = url.searchParams.get('conversationKey');
26
37
  if (!conversationKey) {
27
38
  return Response.json({ error: 'conversationKey is required' }, { status: 400 });
28
39
  }
29
40
 
41
+ const hub = options?.hub ?? assistantEventHub;
42
+ const heartbeatIntervalMs = options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
43
+
30
44
  const mapping = getOrCreateConversation(conversationKey);
31
45
  const encoder = new TextEncoder();
32
- let sub: AssistantEventSubscription | null = null;
46
+
47
+ // ── Eager subscribe ──────────────────────────────────────────────────────
48
+ // Subscribe before creating the ReadableStream so the callback and onEvict
49
+ // closures are in place before events can arrive. `controllerRef` is set
50
+ // synchronously inside ReadableStream's start(), so it is non-null by the
51
+ // time any event or eviction fires.
52
+ // 'self' is the assistantId that RunOrchestrator assigns to all HTTP-run
53
+ // events (see buildAssistantEvent('self', ...) in run-orchestrator.ts).
54
+ let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
55
+ let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
56
+ let sub!: AssistantEventSubscription;
57
+
58
+ function cleanup() {
59
+ if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
60
+ try { controllerRef?.close(); } catch { /* already closed */ }
61
+ }
62
+
63
+ try {
64
+ sub = hub.subscribe(
65
+ { assistantId: 'self', sessionId: mapping.conversationId },
66
+ (event) => {
67
+ const controller = controllerRef;
68
+ if (!controller) return;
69
+ try {
70
+ // Shed stalled consumers: desiredSize <= 0 means the 16-event buffer
71
+ // is full and the client isn't draining it.
72
+ if (controller.desiredSize !== null && controller.desiredSize <= 0) {
73
+ sub.dispose();
74
+ cleanup();
75
+ return;
76
+ }
77
+ controller.enqueue(encoder.encode(formatSseFrame(event)));
78
+ } catch {
79
+ sub.dispose();
80
+ cleanup();
81
+ }
82
+ },
83
+ {
84
+ // Called by the hub when a newer connection evicts this one (capacity
85
+ // management: oldest subscriber out, newest in).
86
+ onEvict: cleanup,
87
+ },
88
+ );
89
+ } catch (err) {
90
+ if (err instanceof RangeError) {
91
+ return Response.json({ error: 'Too many concurrent connections' }, { status: 503 });
92
+ }
93
+ throw err;
94
+ }
33
95
 
34
96
  // Allow up to 16 queued frames before treating the consumer as stalled.
35
97
  // This absorbs normal token-stream bursts without prematurely closing the
36
98
  // connection, while still shedding genuinely slow clients.
37
- const stream = new ReadableStream({
99
+ const stream = new ReadableStream<Uint8Array>({
38
100
  start(controller) {
39
- // 'self' is the assistantId that RunOrchestrator assigns to all HTTP-run events
40
- // (see buildAssistantEvent('self', ...) in run-orchestrator.ts). This endpoint
41
- // is part of the HTTP runtime API, so only HTTP-run events are relevant here.
42
- // IPC/daemon events use a different assistantId ('default') and reach desktop
43
- // clients through a separate channel — they are intentionally excluded.
44
- sub = assistantEventHub.subscribe(
45
- { assistantId: 'self', sessionId: mapping.conversationId },
46
- (event) => {
47
- try {
48
- // Shed stalled consumers: desiredSize <= 0 means the 16-event buffer
49
- // is full and the client isn't draining it.
50
- if (controller.desiredSize !== null && controller.desiredSize <= 0) {
51
- sub?.dispose();
52
- try { controller.close(); } catch { /* already closed */ }
53
- return;
54
- }
55
- controller.enqueue(encoder.encode(formatSseFrame(event)));
56
- } catch {
57
- sub?.dispose();
101
+ controllerRef = controller;
102
+
103
+ // If the client already disconnected before start() ran, clean up
104
+ // immediately the abort event fires once and won't be re-dispatched.
105
+ if (req.signal.aborted) {
106
+ sub.dispose();
107
+ cleanup();
108
+ return;
109
+ }
110
+
111
+ // Send a keep-alive comment on each interval to prevent proxies and
112
+ // load-balancers from treating idle connections as timed out.
113
+ heartbeatTimer = setInterval(() => {
114
+ try {
115
+ // Apply the same slow-consumer guard as the event path: stop
116
+ // feeding heartbeats into a queue the client is not draining.
117
+ if (controller.desiredSize !== null && controller.desiredSize <= 0) {
118
+ sub.dispose();
119
+ cleanup();
120
+ return;
58
121
  }
59
- },
60
- );
122
+ controller.enqueue(encoder.encode(formatSseHeartbeat()));
123
+ } catch {
124
+ // Controller already closed (e.g. client disconnected).
125
+ sub.dispose();
126
+ cleanup();
127
+ }
128
+ }, heartbeatIntervalMs);
61
129
 
62
130
  req.signal.addEventListener('abort', () => {
63
- sub?.dispose();
64
- try { controller.close(); } catch { /* already closed */ }
131
+ sub.dispose();
132
+ cleanup();
65
133
  }, { once: true });
66
134
  },
67
135
  cancel() {
68
- sub?.dispose();
136
+ sub.dispose();
137
+ cleanup();
69
138
  },
70
139
  }, new CountQueuingStrategy({ highWaterMark: 16 }));
71
140
 
@@ -200,13 +200,8 @@ export async function handleAddTrustRule(
200
200
  }
201
201
 
202
202
  try {
203
- // Intentionally omit executionTarget: core tools (bash, file_*, etc.)
204
- // have no executionTarget in their PolicyContext, so a rule with one
205
- // would never match and users would keep getting re-prompted.
206
- addRule(confirmation.toolName, pattern, scope, decision, 100, {
207
- principalKind: confirmation.principalKind,
208
- principalId: confirmation.principalId,
209
- principalVersion: confirmation.principalVersion,
203
+ addRule(confirmation.toolName, pattern, scope, decision, undefined, {
204
+ executionTarget: confirmation.executionTarget,
210
205
  });
211
206
  log.info(
212
207
  { tool: confirmation.toolName, pattern, scope, decision, runId },
@@ -131,9 +131,6 @@ export class RunOrchestrator {
131
131
  executionTarget: msg.executionTarget,
132
132
  allowlistOptions: msg.allowlistOptions,
133
133
  scopeOptions: msg.scopeOptions,
134
- principalKind: msg.principalKind,
135
- principalId: msg.principalId,
136
- principalVersion: msg.principalVersion,
137
134
  persistentDecisionsAllowed: msg.persistentDecisionsAllowed,
138
135
  });
139
136
  this.pending.set(run.id, {
@@ -11,8 +11,24 @@ export interface ScheduleSpec {
11
11
  const SUPPORTED_RRULE_PREFIXES = ['DTSTART', 'RRULE:', 'RDATE', 'EXDATE', 'EXRULE'];
12
12
 
13
13
  function normalizeRruleExpression(expression: string): string {
14
- // Handle escaped newlines from JSON transport
15
- return expression.replace(/\\n/g, '\n').trim();
14
+ // Handle escaped newlines from JSON transport, then uppercase property name
15
+ // prefixes (before the first ';' or ':') on each line so rrulestr() receives
16
+ // the canonical uppercase form regardless of what the caller provided. We
17
+ // stop at the earliest delimiter to preserve case-sensitive parameter values
18
+ // such as timezone names in DTSTART;TZID=America/New_York:...
19
+ return expression
20
+ .replace(/\\n/g, '\n')
21
+ .trim()
22
+ .split(/\r?\n/)
23
+ .map(line => {
24
+ const colonIdx = line.indexOf(':');
25
+ const semiIdx = line.indexOf(';');
26
+ if (colonIdx === -1 && semiIdx === -1) return line;
27
+ // Uppercase only the property name (before the first ';' or ':')
28
+ const nameEnd = semiIdx !== -1 && (colonIdx === -1 || semiIdx < colonIdx) ? semiIdx : colonIdx;
29
+ return line.slice(0, nameEnd).toUpperCase() + line.slice(nameEnd);
30
+ })
31
+ .join('\n');
16
32
  }
17
33
 
18
34
  function parseRruleLines(expression: string): string[] {
@@ -129,6 +145,14 @@ export function computeNextRunAt(spec: ScheduleSpec, nowMs?: number): number {
129
145
  : rrulestr(normalized, { tzid });
130
146
  const next = parsed.after(new Date(now));
131
147
  if (!next) {
148
+ // When after() (exclusive) returns null the rule may still have a
149
+ // terminal occurrence that lands exactly on `now` — e.g. COUNT=1 or the
150
+ // final UNTIL instance. Treat that as "due right now" so claimDueSchedules
151
+ // doesn't silently skip the last run.
152
+ const exactMatch = parsed.before(new Date(now), true);
153
+ if (exactMatch && exactMatch.getTime() === now) {
154
+ return now;
155
+ }
132
156
  throw new Error(`RRULE expression has no upcoming runs after ${new Date(now).toISOString()}`);
133
157
  }
134
158
  return next.getTime();
@@ -60,7 +60,7 @@ export function normalizeScheduleSyntax(input: {
60
60
 
61
61
  // Legacy cron_expression fallback
62
62
  if (input.legacyCronExpression) {
63
- return { syntax: 'cron', expression: input.legacyCronExpression };
63
+ return { syntax: input.syntax ?? 'cron', expression: input.legacyCronExpression };
64
64
  }
65
65
 
66
66
  return null;
@@ -4,8 +4,11 @@ import { Cron } from 'croner';
4
4
  import { getDb } from '../memory/db.js';
5
5
  import { scheduleJobs, scheduleRuns } from '../memory/schema.js';
6
6
  import { computeNextRunAt as computeNextRunAtEngine, isValidScheduleExpression } from './recurrence-engine.js';
7
+ import { getLogger } from '../util/logger.js';
7
8
  import type { ScheduleSyntax } from './recurrence-types.js';
8
9
 
10
+ const logger = getLogger('schedule-store');
11
+
9
12
  export interface ScheduleJob {
10
13
  id: string;
11
14
  name: string;
@@ -216,9 +219,15 @@ export function claimDueSchedules(now: number): ScheduleJob[] {
216
219
  expression: row.cronExpression,
217
220
  timezone: row.timezone,
218
221
  });
219
- } catch {
220
- // Finite schedule with no future runs still claim the current due
221
- // run but disable the schedule so it doesn't fire again.
222
+ } catch (err) {
223
+ const msg = err instanceof Error ? err.message : String(err);
224
+ if (!msg.includes('no upcoming runs')) {
225
+ // Log but don't abort — one bad schedule shouldn't block everything
226
+ logger.warn({ err, scheduleId: row.id }, 'Failed to compute next run for schedule');
227
+ continue;
228
+ }
229
+ // Expired schedules fire their final pending due run then auto-disable,
230
+ // ensuring no due run is silently dropped.
222
231
  newNextRunAt = null;
223
232
  exhausted = true;
224
233
  }
@@ -81,6 +81,13 @@ const PATTERNS: SecretPattern[] = [
81
81
  regex: /(https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+)/g,
82
82
  },
83
83
 
84
+ // -- Telegram --
85
+ {
86
+ type: 'Telegram Bot Token',
87
+ // Format: <bot_id>:<secret> where bot_id is 8-10 digits and secret is 35 alphanumeric/dash/underscore chars
88
+ regex: /\b([0-9]{8,10}:[A-Za-z0-9_-]{35})(?=[^A-Za-z0-9_-]|$)/g,
89
+ },
90
+
84
91
  // -- Anthropic --
85
92
  {
86
93
  type: 'Anthropic API Key',
@@ -44,7 +44,5 @@ export function buildTaskRules(taskRunId: string, requiredTools: string[], _work
44
44
  allowHighRisk: true,
45
45
  priority: 75,
46
46
  createdAt: Date.now(),
47
- principalKind: 'task',
48
- principalId: taskRunId,
49
47
  }));
50
48
  }
@@ -1,7 +1,8 @@
1
1
  import { createSchedule } from '../schedule/schedule-store.js';
2
2
 
3
3
  /**
4
- * Create a recurrence schedule that runs a task on a cron or RRULE expression.
4
+ * Create a cron schedule that runs a task on a recurring cron expression.
5
+ * RRULE syntax is supported at the store layer but this helper currently defaults to cron.
5
6
  * The scheduler detects the `run_task:<taskId>` message format
6
7
  * and delegates to runTask() instead of processMessage().
7
8
  */
@@ -24,6 +24,11 @@ const definition: ToolDefinition = {
24
24
  type: 'string',
25
25
  description: 'Additional context for the conversation',
26
26
  },
27
+ caller_identity_mode: {
28
+ type: 'string',
29
+ enum: ['assistant_number', 'user_number'],
30
+ description: 'Which phone number to use as the caller ID. assistant_number uses the AI assistant\'s Twilio number; user_number uses the user\'s verified personal number.',
31
+ },
27
32
  },
28
33
  required: ['phone_number', 'task'],
29
34
  },
@@ -49,6 +54,7 @@ class CallStartTool implements Tool {
49
54
  task: input.task as string,
50
55
  context: input.context as string | undefined,
51
56
  conversationId: context.conversationId,
57
+ callerIdentityMode: input.caller_identity_mode as 'assistant_number' | 'user_number' | undefined,
52
58
  });
53
59
 
54
60
  if (!result.ok) {
@@ -61,6 +67,8 @@ class CallStartTool implements Tool {
61
67
  ` Call Session ID: ${result.session.id}`,
62
68
  ` Call SID: ${result.callSid}`,
63
69
  ` To: ${result.session.toNumber}`,
70
+ ` From: ${result.session.fromNumber}`,
71
+ ` Caller Identity Mode: ${result.callerIdentityMode}`,
64
72
  ` Status: initiated`,
65
73
  '',
66
74
  'The AI voice assistant is now placing the call. Use call_status to check progress.',
@@ -0,0 +1,21 @@
1
+ import type { ExecutionTarget } from './types.js';
2
+ import { getTool } from './registry.js';
3
+
4
+ export function resolveExecutionTarget(toolName: string): ExecutionTarget {
5
+ const tool = getTool(toolName);
6
+ // Manifest-declared execution target is authoritative — check it first so
7
+ // skill tools with host_/computer_use_ prefixes aren't mis-classified.
8
+ if (tool?.executionTarget) {
9
+ return tool.executionTarget;
10
+ }
11
+ // Check the tool's executionMode metadata — proxy tools run on the connected
12
+ // client (host), not inside the sandbox.
13
+ if (tool?.executionMode === 'proxy') {
14
+ return 'host';
15
+ }
16
+ // Prefix heuristics for core tools that don't declare an explicit target.
17
+ if (toolName.startsWith('host_') || toolName.startsWith('computer_use_')) {
18
+ return 'host';
19
+ }
20
+ return 'sandbox';
21
+ }
@@ -0,0 +1,49 @@
1
+ import type { ToolExecutionResult } from './types.js';
2
+
3
+ const TIMEOUT_SENTINEL = Symbol('tool-timeout');
4
+
5
+ export const DEFAULT_TOOL_TIMEOUT_SEC = 120;
6
+
7
+ /**
8
+ * Convert a config-provided seconds value to a safe milliseconds value,
9
+ * falling back to the default if the input is NaN, non-finite, zero, or negative.
10
+ */
11
+ export function safeTimeoutMs(sec: unknown): number {
12
+ const n = Number(sec);
13
+ if (!Number.isFinite(n) || n <= 0) {
14
+ return DEFAULT_TOOL_TIMEOUT_SEC * 1000;
15
+ }
16
+ return n * 1000;
17
+ }
18
+
19
+ /**
20
+ * Race a tool execution promise against a timeout. Returns a timeout error
21
+ * result instead of throwing so the agent loop can continue gracefully.
22
+ */
23
+ export async function executeWithTimeout(
24
+ promise: Promise<ToolExecutionResult>,
25
+ timeoutMs: number,
26
+ toolName: string,
27
+ ): Promise<ToolExecutionResult> {
28
+ // Guard against NaN/invalid values that would cause setTimeout to fire immediately
29
+ const safeMs = Number.isFinite(timeoutMs) && timeoutMs > 0
30
+ ? timeoutMs
31
+ : DEFAULT_TOOL_TIMEOUT_SEC * 1000;
32
+ let timeoutHandle: ReturnType<typeof setTimeout>;
33
+ const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) => {
34
+ timeoutHandle = setTimeout(() => resolve(TIMEOUT_SENTINEL), safeMs);
35
+ });
36
+ try {
37
+ const result = await Promise.race([promise, timeoutPromise]);
38
+ if (result === TIMEOUT_SENTINEL) {
39
+ const sec = Math.round(safeMs / 1000);
40
+ return {
41
+ content: `Tool "${toolName}" timed out after ${sec}s. The operation may still be running in the background. Consider increasing timeouts.toolExecutionTimeoutSec in the config.`,
42
+ isError: true,
43
+ };
44
+ }
45
+ return result;
46
+ } finally {
47
+ clearTimeout(timeoutHandle!);
48
+ }
49
+ }
@@ -1,8 +1,7 @@
1
1
  import { readFileSync, existsSync, statSync } from 'node:fs';
2
2
  import { getTool, getAllTools } from './registry.js';
3
- import type { ExecutionTarget, Tool, ToolContext, ToolExecutionResult, ToolLifecycleEvent } from './types.js';
3
+ import type { ToolContext, ToolExecutionResult, ToolLifecycleEvent } from './types.js';
4
4
  import { RiskLevel } from '../permissions/types.js';
5
- import type { PolicyContext } from '../permissions/types.js';
6
5
  import { check, classifyRisk, generateAllowlistOptions, generateScopeOptions } from '../permissions/checker.js';
7
6
  import { addRule } from '../permissions/trust-store.js';
8
7
  import { PermissionPrompter } from '../permissions/prompter.js';
@@ -18,6 +17,9 @@ import { scanText, redactSecrets } from '../security/secret-scanner.js';
18
17
  import { redactSensitiveFields } from '../security/redaction.js';
19
18
  import { getHookManager } from '../hooks/manager.js';
20
19
  import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
20
+ import { safeTimeoutMs, executeWithTimeout } from './execution-timeout.js';
21
+ import { buildPolicyContext } from './policy-context.js';
22
+ import { resolveExecutionTarget } from './execution-target.js';
21
23
 
22
24
  const log = getLogger('tool-executor');
23
25
 
@@ -196,7 +198,7 @@ export class ToolExecutor {
196
198
  }
197
199
 
198
200
  // Need user approval
199
- const allowlistOptions = generateAllowlistOptions(name, input);
201
+ const allowlistOptions = await generateAllowlistOptions(name, input);
200
202
  const scopeOptions = generateScopeOptions(context.workingDir, name);
201
203
 
202
204
  // Compute preview diff for file tools so the user sees what will change
@@ -253,11 +255,6 @@ export class ToolExecutor {
253
255
  sandboxed,
254
256
  context.conversationId,
255
257
  executionTarget,
256
- policyContext?.principal ? {
257
- kind: policyContext.principal.kind,
258
- id: policyContext.principal.id,
259
- version: policyContext.principal.version,
260
- } : undefined,
261
258
  persistentDecisionsAllowed,
262
259
  );
263
260
 
@@ -325,9 +322,6 @@ export class ToolExecutor {
325
322
  ) {
326
323
  const ruleOptions: {
327
324
  allowHighRisk?: boolean;
328
- principalKind?: string;
329
- principalId?: string;
330
- principalVersion?: string;
331
325
  executionTarget?: string;
332
326
  } = {};
333
327
 
@@ -335,19 +329,6 @@ export class ToolExecutor {
335
329
  ruleOptions.allowHighRisk = true;
336
330
  }
337
331
 
338
- // Capture the principal context from the tool so the saved rule
339
- // is scoped to the specific skill/version that was approved.
340
- if (policyContext?.principal) {
341
- if (policyContext.principal.kind != null) {
342
- ruleOptions.principalKind = policyContext.principal.kind;
343
- }
344
- if (policyContext.principal.id != null) {
345
- ruleOptions.principalId = policyContext.principal.id;
346
- }
347
- if (policyContext.principal.version != null) {
348
- ruleOptions.principalVersion = policyContext.principal.version;
349
- }
350
- }
351
332
  if (policyContext?.executionTarget != null) {
352
333
  ruleOptions.executionTarget = policyContext.executionTarget;
353
334
  }
@@ -393,11 +374,7 @@ export class ToolExecutor {
393
374
  const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
394
375
  const toolTimeoutMs = safeTimeoutMs(rawTimeoutSec);
395
376
 
396
- // Enrich context with principal so tools (e.g. claude_code) can
397
- // forward it through sub-tool confirmation requests.
398
- const execContext = policyContext?.principal
399
- ? { ...context, principal: policyContext.principal }
400
- : context;
377
+ const execContext = context;
401
378
 
402
379
  if (tool.executionMode === 'proxy') {
403
380
  if (!context.proxyToolResolver) {
@@ -589,7 +566,6 @@ export class ToolExecutor {
589
566
  undefined, // not sandboxed
590
567
  context.conversationId,
591
568
  executionTarget,
592
- undefined, // no principal
593
569
  false, // no persistent decisions
594
570
  );
595
571
 
@@ -756,111 +732,6 @@ export function isSideEffectTool(toolName: string, input?: Record<string, unknow
756
732
  return false;
757
733
  }
758
734
 
759
- const TIMEOUT_SENTINEL = Symbol('tool-timeout');
760
-
761
- const DEFAULT_TOOL_TIMEOUT_SEC = 120;
762
-
763
- /**
764
- * Convert a config-provided seconds value to a safe milliseconds value,
765
- * falling back to the default if the input is NaN, non-finite, zero, or negative.
766
- */
767
- function safeTimeoutMs(sec: unknown): number {
768
- const n = Number(sec);
769
- if (!Number.isFinite(n) || n <= 0) {
770
- return DEFAULT_TOOL_TIMEOUT_SEC * 1000;
771
- }
772
- return n * 1000;
773
- }
774
-
775
- /**
776
- * Race a tool execution promise against a timeout. Returns a timeout error
777
- * result instead of throwing so the agent loop can continue gracefully.
778
- */
779
- async function executeWithTimeout(
780
- promise: Promise<ToolExecutionResult>,
781
- timeoutMs: number,
782
- toolName: string,
783
- ): Promise<ToolExecutionResult> {
784
- // Guard against NaN/invalid values that would cause setTimeout to fire immediately
785
- const safeMs = Number.isFinite(timeoutMs) && timeoutMs > 0
786
- ? timeoutMs
787
- : DEFAULT_TOOL_TIMEOUT_SEC * 1000;
788
- let timeoutHandle: ReturnType<typeof setTimeout>;
789
- const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) => {
790
- timeoutHandle = setTimeout(() => resolve(TIMEOUT_SENTINEL), safeMs);
791
- });
792
- try {
793
- const result = await Promise.race([promise, timeoutPromise]);
794
- if (result === TIMEOUT_SENTINEL) {
795
- const sec = Math.round(safeMs / 1000);
796
- return {
797
- content: `Tool "${toolName}" timed out after ${sec}s. The operation may still be running in the background. Consider increasing timeouts.toolExecutionTimeoutSec in the config.`,
798
- isError: true,
799
- };
800
- }
801
- return result;
802
- } finally {
803
- clearTimeout(timeoutHandle!);
804
- }
805
- }
806
-
807
- /**
808
- * Build a PolicyContext from tool metadata and execution context. Skill-origin
809
- * tools carry a principal identifying the owning skill. When executing within
810
- * a task run, ephemeral permission rules are included so pre-approved tools
811
- * are auto-allowed without prompting.
812
- */
813
- function buildPolicyContext(tool: Tool, context?: ToolContext): PolicyContext | undefined {
814
- const ephemeralRules = context?.taskRunId
815
- ? getTaskRunRules(context.taskRunId)
816
- : undefined;
817
-
818
- if (tool.origin === 'skill') {
819
- return {
820
- principal: {
821
- kind: 'skill',
822
- id: tool.ownerSkillId,
823
- version: tool.ownerSkillVersionHash,
824
- },
825
- executionTarget: tool.executionTarget,
826
- ephemeralRules: ephemeralRules?.length ? ephemeralRules : undefined,
827
- };
828
- }
829
-
830
- // For non-skill tools in a task run, create a context with task principal
831
- // and ephemeral rules so pre-approved tools are honored.
832
- if (context?.taskRunId && ephemeralRules?.length) {
833
- return {
834
- principal: {
835
- kind: 'task',
836
- id: context.taskRunId,
837
- },
838
- ephemeralRules,
839
- };
840
- }
841
-
842
- return undefined;
843
- }
844
-
845
- function resolveExecutionTarget(toolName: string): ExecutionTarget {
846
- const tool = getTool(toolName);
847
- // Manifest-declared execution target is authoritative — check it first so
848
- // skill tools with host_/computer_use_ prefixes aren't mis-classified.
849
- if (tool?.executionTarget) {
850
- return tool.executionTarget;
851
- }
852
- // Check the tool's executionMode metadata — proxy tools run on the connected
853
- // client (host), not inside the sandbox.
854
- if (tool?.executionMode === 'proxy') {
855
- return 'host';
856
- }
857
- // Prefix heuristics for core tools that don't declare an explicit target.
858
- if (toolName.startsWith('host_') || toolName.startsWith('computer_use_')) {
859
- return 'host';
860
- }
861
- return 'sandbox';
862
- }
863
-
864
735
  /**
865
736
  * Sanitize tool inputs before they are emitted in lifecycle events and hooks.
866
737
  * Applies recursive field-level redaction for known-sensitive keys.