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.
- package/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +171 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +402 -5
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +271 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +28 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +96 -8
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +97 -0
- package/src/calls/elevenlabs-config.ts +31 -0
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +50 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +114 -0
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +26 -2
- package/src/config/schema.ts +178 -9
- package/src/config/types.ts +3 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/defaults.ts +11 -0
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/conversation-routes.ts +12 -5
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- 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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
64
|
-
|
|
131
|
+
sub.dispose();
|
|
132
|
+
cleanup();
|
|
65
133
|
}, { once: true });
|
|
66
134
|
},
|
|
67
135
|
cancel() {
|
|
68
|
-
sub
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createSchedule } from '../schedule/schedule-store.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Create a
|
|
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
|
+
}
|
package/src/tools/executor.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { getTool, getAllTools } from './registry.js';
|
|
3
|
-
import type {
|
|
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
|
-
|
|
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.
|