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
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from './routes/channel-routes.js';
|
|
45
45
|
import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
|
|
46
46
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
47
|
+
import * as externalConversationStore from '../memory/external-conversation-store.js';
|
|
47
48
|
import * as attachmentsStore from '../memory/attachments-store.js';
|
|
48
49
|
import { renderHistoryContent } from '../daemon/handlers.js';
|
|
49
50
|
import { deliverChannelReply } from './gateway-client.js';
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
handleGetCallStatus,
|
|
61
62
|
handleCancelCall,
|
|
62
63
|
handleAnswerCall,
|
|
64
|
+
handleInstructionCall,
|
|
63
65
|
} from './routes/call-routes.js';
|
|
64
66
|
import {
|
|
65
67
|
handleVoiceWebhook,
|
|
@@ -616,13 +618,28 @@ export class RuntimeHttpServer {
|
|
|
616
618
|
if (endpoint === 'conversations' && req.method === 'GET') {
|
|
617
619
|
const limit = Number(url.searchParams.get('limit') ?? 50);
|
|
618
620
|
const conversations = conversationStore.listConversations(limit);
|
|
621
|
+
const bindings = externalConversationStore.getBindingsForConversations(
|
|
622
|
+
conversations.map((c) => c.id),
|
|
623
|
+
);
|
|
619
624
|
return Response.json({
|
|
620
|
-
sessions: conversations.map((c) =>
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
625
|
+
sessions: conversations.map((c) => {
|
|
626
|
+
const binding = bindings.get(c.id);
|
|
627
|
+
return {
|
|
628
|
+
id: c.id,
|
|
629
|
+
title: c.title ?? 'Untitled',
|
|
630
|
+
updatedAt: c.updatedAt,
|
|
631
|
+
threadType: c.threadType === 'private' ? 'private' : 'standard',
|
|
632
|
+
...(binding ? {
|
|
633
|
+
channelBinding: {
|
|
634
|
+
sourceChannel: binding.sourceChannel,
|
|
635
|
+
externalChatId: binding.externalChatId,
|
|
636
|
+
externalUserId: binding.externalUserId,
|
|
637
|
+
displayName: binding.displayName,
|
|
638
|
+
username: binding.username,
|
|
639
|
+
},
|
|
640
|
+
} : {}),
|
|
641
|
+
};
|
|
642
|
+
}),
|
|
626
643
|
});
|
|
627
644
|
}
|
|
628
645
|
|
|
@@ -700,7 +717,7 @@ export class RuntimeHttpServer {
|
|
|
700
717
|
}
|
|
701
718
|
|
|
702
719
|
if (endpoint === 'channels/inbound' && req.method === 'POST') {
|
|
703
|
-
return await handleChannelInbound(req, this.processMessage, this.bearerToken);
|
|
720
|
+
return await handleChannelInbound(req, this.processMessage, this.bearerToken, this.runOrchestrator);
|
|
704
721
|
}
|
|
705
722
|
|
|
706
723
|
if (endpoint === 'channels/delivery-ack' && req.method === 'POST') {
|
|
@@ -720,8 +737,8 @@ export class RuntimeHttpServer {
|
|
|
720
737
|
return await handleStartCall(req);
|
|
721
738
|
}
|
|
722
739
|
|
|
723
|
-
// Match calls/:callSessionId and calls/:callSessionId/cancel, calls/:callSessionId/answer
|
|
724
|
-
const callsMatch = endpoint.match(/^calls\/([^/]+?)(\/cancel|\/answer)?$/);
|
|
740
|
+
// Match calls/:callSessionId and calls/:callSessionId/cancel, calls/:callSessionId/answer, calls/:callSessionId/instruction
|
|
741
|
+
const callsMatch = endpoint.match(/^calls\/([^/]+?)(\/cancel|\/answer|\/instruction)?$/);
|
|
725
742
|
if (callsMatch) {
|
|
726
743
|
const callSessionId = callsMatch[1];
|
|
727
744
|
// Skip known sub-paths that are handled elsewhere (twilio, relay)
|
|
@@ -732,6 +749,9 @@ export class RuntimeHttpServer {
|
|
|
732
749
|
if (callsMatch[2] === '/answer' && req.method === 'POST') {
|
|
733
750
|
return await handleAnswerCall(req, callSessionId);
|
|
734
751
|
}
|
|
752
|
+
if (callsMatch[2] === '/instruction' && req.method === 'POST') {
|
|
753
|
+
return await handleInstructionCall(req, callSessionId);
|
|
754
|
+
}
|
|
735
755
|
if (!callsMatch[2] && req.method === 'GET') {
|
|
736
756
|
return handleGetCallStatus(callSessionId);
|
|
737
757
|
}
|
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
* GET /v1/calls/:callSessionId — get call status
|
|
6
6
|
* POST /v1/calls/:callSessionId/cancel — cancel a call
|
|
7
7
|
* POST /v1/calls/:callSessionId/answer — answer a pending question
|
|
8
|
+
* POST /v1/calls/:callSessionId/instruction — relay an instruction to an active call
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import { startCall, getCallStatus, cancelCall, answerCall } from '../../calls/call-domain.js';
|
|
11
|
+
import { startCall, getCallStatus, cancelCall, answerCall, relayInstruction } from '../../calls/call-domain.js';
|
|
11
12
|
import { getConfig } from '../../config/loader.js';
|
|
13
|
+
import { VALID_CALLER_IDENTITY_MODES } from '../../config/schema.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* POST /v1/calls/start
|
|
15
17
|
*
|
|
16
|
-
* Body: { phoneNumber: string; task: string; context?: string; conversationId: string }
|
|
18
|
+
* Body: { phoneNumber: string; task: string; context?: string; conversationId: string; callerIdentityMode?: 'assistant_number' | 'user_number' }
|
|
17
19
|
*/
|
|
18
20
|
export async function handleStartCall(req: Request): Promise<Response> {
|
|
19
21
|
if (!getConfig().calls.enabled) {
|
|
@@ -28,6 +30,7 @@ export async function handleStartCall(req: Request): Promise<Response> {
|
|
|
28
30
|
task?: string;
|
|
29
31
|
context?: string;
|
|
30
32
|
conversationId?: string;
|
|
33
|
+
callerIdentityMode?: 'assistant_number' | 'user_number';
|
|
31
34
|
};
|
|
32
35
|
try {
|
|
33
36
|
body = await req.json() as typeof body;
|
|
@@ -35,15 +38,28 @@ export async function handleStartCall(req: Request): Promise<Response> {
|
|
|
35
38
|
return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
42
|
+
return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
if (!body.conversationId) {
|
|
39
46
|
return Response.json({ error: 'conversationId is required' }, { status: 400 });
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
if (body.callerIdentityMode != null &&
|
|
50
|
+
!(VALID_CALLER_IDENTITY_MODES as readonly string[]).includes(body.callerIdentityMode as string)) {
|
|
51
|
+
return Response.json(
|
|
52
|
+
{ error: `Invalid callerIdentityMode: "${body.callerIdentityMode}". Must be one of: ${VALID_CALLER_IDENTITY_MODES.join(', ')}` },
|
|
53
|
+
{ status: 400 },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
const result = await startCall({
|
|
43
58
|
phoneNumber: body.phoneNumber ?? '',
|
|
44
59
|
task: body.task ?? '',
|
|
45
60
|
context: body.context,
|
|
46
61
|
conversationId: body.conversationId,
|
|
62
|
+
callerIdentityMode: body.callerIdentityMode,
|
|
47
63
|
});
|
|
48
64
|
|
|
49
65
|
if (!result.ok) {
|
|
@@ -56,6 +72,7 @@ export async function handleStartCall(req: Request): Promise<Response> {
|
|
|
56
72
|
status: result.session.status,
|
|
57
73
|
toNumber: result.session.toNumber,
|
|
58
74
|
fromNumber: result.session.fromNumber,
|
|
75
|
+
callerIdentityMode: result.callerIdentityMode,
|
|
59
76
|
}, { status: 201 });
|
|
60
77
|
}
|
|
61
78
|
|
|
@@ -127,6 +144,10 @@ export async function handleAnswerCall(req: Request, callSessionId: string): Pro
|
|
|
127
144
|
return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
|
|
128
145
|
}
|
|
129
146
|
|
|
147
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
148
|
+
return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
|
|
149
|
+
}
|
|
150
|
+
|
|
130
151
|
const result = await answerCall({
|
|
131
152
|
callSessionId,
|
|
132
153
|
answer: body.answer ?? '',
|
|
@@ -138,3 +159,32 @@ export async function handleAnswerCall(req: Request, callSessionId: string): Pro
|
|
|
138
159
|
|
|
139
160
|
return Response.json({ ok: true, questionId: result.questionId });
|
|
140
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* POST /v1/calls/:callSessionId/instruction
|
|
165
|
+
*
|
|
166
|
+
* Body: { instruction: string }
|
|
167
|
+
*/
|
|
168
|
+
export async function handleInstructionCall(req: Request, callSessionId: string): Promise<Response> {
|
|
169
|
+
let body: { instruction?: string };
|
|
170
|
+
try {
|
|
171
|
+
body = await req.json() as typeof body;
|
|
172
|
+
} catch {
|
|
173
|
+
return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
177
|
+
return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = await relayInstruction({
|
|
181
|
+
callSessionId,
|
|
182
|
+
instructionText: body.instruction ?? '',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
return Response.json({ error: result.error }, { status: result.status ?? 500 });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Response.json({ ok: true });
|
|
190
|
+
}
|
|
@@ -6,11 +6,22 @@ import { deleteConversationKey } from '../../memory/conversation-key-store.js';
|
|
|
6
6
|
import * as conversationStore from '../../memory/conversation-store.js';
|
|
7
7
|
import * as attachmentsStore from '../../memory/attachments-store.js';
|
|
8
8
|
import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
|
|
9
|
+
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
10
|
+
import { getPendingConfirmationsByConversation } from '../../memory/runs-store.js';
|
|
9
11
|
import { renderHistoryContent } from '../../daemon/handlers.js';
|
|
10
12
|
import { checkIngressForSecrets } from '../../security/secret-ingress.js';
|
|
11
13
|
import { IngressBlockedError } from '../../util/errors.js';
|
|
12
14
|
import { getLogger } from '../../util/logger.js';
|
|
13
|
-
import { deliverChannelReply } from '../gateway-client.js';
|
|
15
|
+
import { deliverChannelReply, deliverApprovalPrompt } from '../gateway-client.js';
|
|
16
|
+
import { parseApprovalDecision } from '../channel-approval-parser.js';
|
|
17
|
+
import {
|
|
18
|
+
getChannelApprovalPrompt,
|
|
19
|
+
buildApprovalUIMetadata,
|
|
20
|
+
handleChannelDecision,
|
|
21
|
+
buildReminderPrompt,
|
|
22
|
+
} from '../channel-approvals.js';
|
|
23
|
+
import type { ApprovalAction, ApprovalDecisionResult } from '../channel-approval-types.js';
|
|
24
|
+
import type { RunOrchestrator } from '../run-orchestrator.js';
|
|
14
25
|
import type {
|
|
15
26
|
MessageProcessor,
|
|
16
27
|
RuntimeAttachmentMetadata,
|
|
@@ -18,6 +29,32 @@ import type {
|
|
|
18
29
|
|
|
19
30
|
const log = getLogger('runtime-http');
|
|
20
31
|
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Feature flag
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export function isChannelApprovalsEnabled(): boolean {
|
|
37
|
+
return process.env.CHANNEL_APPROVALS_ENABLED === 'true';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Callback data parser — format: "apr:<runId>:<action>"
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const VALID_ACTIONS: ReadonlySet<string> = new Set<string>([
|
|
45
|
+
'approve_once',
|
|
46
|
+
'approve_always',
|
|
47
|
+
'reject',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function parseCallbackData(data: string): ApprovalDecisionResult | null {
|
|
51
|
+
const parts = data.split(':');
|
|
52
|
+
if (parts.length < 3 || parts[0] !== 'apr') return null;
|
|
53
|
+
const action = parts.slice(2).join(':');
|
|
54
|
+
if (!VALID_ACTIONS.has(action)) return null;
|
|
55
|
+
return { action: action as ApprovalAction, source: 'telegram_button' };
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
export async function handleDeleteConversation(req: Request): Promise<Response> {
|
|
22
59
|
const body = await req.json() as {
|
|
23
60
|
sourceChannel?: string;
|
|
@@ -35,6 +72,7 @@ export async function handleDeleteConversation(req: Request): Promise<Response>
|
|
|
35
72
|
|
|
36
73
|
const conversationKey = `${sourceChannel}:${externalChatId}`;
|
|
37
74
|
deleteConversationKey(conversationKey);
|
|
75
|
+
externalConversationStore.deleteBindingByChannelChat(sourceChannel, externalChatId);
|
|
38
76
|
|
|
39
77
|
return Response.json({ ok: true });
|
|
40
78
|
}
|
|
@@ -43,6 +81,7 @@ export async function handleChannelInbound(
|
|
|
43
81
|
req: Request,
|
|
44
82
|
processMessage?: MessageProcessor,
|
|
45
83
|
bearerToken?: string,
|
|
84
|
+
runOrchestrator?: RunOrchestrator,
|
|
46
85
|
): Promise<Response> {
|
|
47
86
|
const body = await req.json() as {
|
|
48
87
|
sourceChannel?: string;
|
|
@@ -56,6 +95,8 @@ export async function handleChannelInbound(
|
|
|
56
95
|
senderUsername?: string;
|
|
57
96
|
sourceMetadata?: Record<string, unknown>;
|
|
58
97
|
replyCallbackUrl?: string;
|
|
98
|
+
callbackQueryId?: string;
|
|
99
|
+
callbackData?: string;
|
|
59
100
|
};
|
|
60
101
|
|
|
61
102
|
const {
|
|
@@ -179,6 +220,16 @@ export async function handleChannelInbound(
|
|
|
179
220
|
{ sourceMessageId },
|
|
180
221
|
);
|
|
181
222
|
|
|
223
|
+
// Upsert external conversation binding with sender metadata
|
|
224
|
+
externalConversationStore.upsertBinding({
|
|
225
|
+
conversationId: result.conversationId,
|
|
226
|
+
sourceChannel,
|
|
227
|
+
externalChatId,
|
|
228
|
+
externalUserId: body.senderExternalUserId ?? null,
|
|
229
|
+
displayName: body.senderName ?? null,
|
|
230
|
+
username: body.senderUsername ?? null,
|
|
231
|
+
});
|
|
232
|
+
|
|
182
233
|
const metadataHintsRaw = sourceMetadata?.hints;
|
|
183
234
|
const metadataHints = Array.isArray(metadataHintsRaw)
|
|
184
235
|
? metadataHintsRaw.filter((hint): hint is string => typeof hint === 'string' && hint.trim().length > 0)
|
|
@@ -189,6 +240,33 @@ export async function handleChannelInbound(
|
|
|
189
240
|
|
|
190
241
|
const replyCallbackUrl = body.replyCallbackUrl;
|
|
191
242
|
|
|
243
|
+
// ── Approval interception (gated behind feature flag) ──
|
|
244
|
+
if (
|
|
245
|
+
isChannelApprovalsEnabled() &&
|
|
246
|
+
runOrchestrator &&
|
|
247
|
+
replyCallbackUrl &&
|
|
248
|
+
!result.duplicate
|
|
249
|
+
) {
|
|
250
|
+
const approvalResult = await handleApprovalInterception({
|
|
251
|
+
conversationId: result.conversationId,
|
|
252
|
+
callbackData: body.callbackData,
|
|
253
|
+
content: trimmedContent,
|
|
254
|
+
externalChatId,
|
|
255
|
+
replyCallbackUrl,
|
|
256
|
+
bearerToken,
|
|
257
|
+
orchestrator: runOrchestrator,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (approvalResult.handled) {
|
|
261
|
+
return Response.json({
|
|
262
|
+
accepted: true,
|
|
263
|
+
duplicate: false,
|
|
264
|
+
eventId: result.eventId,
|
|
265
|
+
approval: approvalResult.type,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
192
270
|
// For new (non-duplicate) messages, run the secret ingress check
|
|
193
271
|
// synchronously, then fire off the agent loop in the background.
|
|
194
272
|
if (!result.duplicate && processMessage) {
|
|
@@ -217,21 +295,40 @@ export async function handleChannelInbound(
|
|
|
217
295
|
throw new IngressBlockedError(ingressCheck.userNotice!, ingressCheck.detectedTypes);
|
|
218
296
|
}
|
|
219
297
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
298
|
+
// When approval flow is enabled and we have an orchestrator, use the
|
|
299
|
+
// orchestrator-backed path which properly intercepts confirmation_request
|
|
300
|
+
// events and sends proactive approval prompts to the channel.
|
|
301
|
+
const useApprovalPath =
|
|
302
|
+
isChannelApprovalsEnabled() && runOrchestrator && replyCallbackUrl;
|
|
303
|
+
|
|
304
|
+
if (useApprovalPath) {
|
|
305
|
+
processChannelMessageWithApprovals({
|
|
306
|
+
orchestrator: runOrchestrator,
|
|
307
|
+
conversationId: result.conversationId,
|
|
308
|
+
eventId: result.eventId,
|
|
309
|
+
content: content ?? '',
|
|
310
|
+
attachmentIds: hasAttachments ? attachmentIds : undefined,
|
|
311
|
+
externalChatId,
|
|
312
|
+
replyCallbackUrl,
|
|
313
|
+
bearerToken,
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
// Fire-and-forget: process the message and deliver the reply in the background.
|
|
317
|
+
// The HTTP response returns immediately so the gateway webhook is not blocked.
|
|
318
|
+
processChannelMessageInBackground({
|
|
319
|
+
processMessage,
|
|
320
|
+
conversationId: result.conversationId,
|
|
321
|
+
eventId: result.eventId,
|
|
322
|
+
content: content ?? '',
|
|
323
|
+
attachmentIds: hasAttachments ? attachmentIds : undefined,
|
|
324
|
+
sourceChannel,
|
|
325
|
+
externalChatId,
|
|
326
|
+
metadataHints,
|
|
327
|
+
metadataUxBrief,
|
|
328
|
+
replyCallbackUrl,
|
|
329
|
+
bearerToken,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
235
332
|
}
|
|
236
333
|
|
|
237
334
|
return Response.json({
|
|
@@ -298,6 +395,189 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
298
395
|
})();
|
|
299
396
|
}
|
|
300
397
|
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Orchestrator-backed channel processing with approval prompt delivery
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
const RUN_POLL_INTERVAL_MS = 500;
|
|
403
|
+
const RUN_POLL_MAX_WAIT_MS = 300_000; // 5 minutes
|
|
404
|
+
|
|
405
|
+
interface ApprovalProcessingParams {
|
|
406
|
+
orchestrator: RunOrchestrator;
|
|
407
|
+
conversationId: string;
|
|
408
|
+
eventId: string;
|
|
409
|
+
content: string;
|
|
410
|
+
attachmentIds?: string[];
|
|
411
|
+
externalChatId: string;
|
|
412
|
+
replyCallbackUrl: string;
|
|
413
|
+
bearerToken?: string;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Process a channel message using the run orchestrator so that
|
|
418
|
+
* `confirmation_request` events are intercepted and written to the
|
|
419
|
+
* runs store. Polls the run until it reaches a terminal state,
|
|
420
|
+
* sending approval prompts when `needs_confirmation` is detected.
|
|
421
|
+
*/
|
|
422
|
+
function processChannelMessageWithApprovals(params: ApprovalProcessingParams): void {
|
|
423
|
+
const {
|
|
424
|
+
orchestrator,
|
|
425
|
+
conversationId,
|
|
426
|
+
eventId,
|
|
427
|
+
content,
|
|
428
|
+
attachmentIds,
|
|
429
|
+
externalChatId,
|
|
430
|
+
replyCallbackUrl,
|
|
431
|
+
bearerToken,
|
|
432
|
+
} = params;
|
|
433
|
+
|
|
434
|
+
(async () => {
|
|
435
|
+
try {
|
|
436
|
+
const run = await orchestrator.startRun(conversationId, content, attachmentIds);
|
|
437
|
+
|
|
438
|
+
// Poll the run until it reaches a terminal state, delivering approval
|
|
439
|
+
// prompts when it transitions to needs_confirmation.
|
|
440
|
+
const startTime = Date.now();
|
|
441
|
+
let lastStatus = run.status;
|
|
442
|
+
|
|
443
|
+
while (Date.now() - startTime < RUN_POLL_MAX_WAIT_MS) {
|
|
444
|
+
await new Promise((resolve) => setTimeout(resolve, RUN_POLL_INTERVAL_MS));
|
|
445
|
+
|
|
446
|
+
const current = orchestrator.getRun(run.id);
|
|
447
|
+
if (!current) break;
|
|
448
|
+
|
|
449
|
+
if (current.status === 'needs_confirmation' && lastStatus !== 'needs_confirmation') {
|
|
450
|
+
// Run just transitioned to needs_confirmation — send approval prompt
|
|
451
|
+
const prompt = getChannelApprovalPrompt(conversationId);
|
|
452
|
+
if (prompt) {
|
|
453
|
+
const pending = getPendingConfirmationsByConversation(conversationId);
|
|
454
|
+
if (pending.length > 0) {
|
|
455
|
+
const uiMetadata = buildApprovalUIMetadata(prompt, pending[0]);
|
|
456
|
+
try {
|
|
457
|
+
await deliverApprovalPrompt(
|
|
458
|
+
replyCallbackUrl,
|
|
459
|
+
externalChatId,
|
|
460
|
+
prompt.promptText,
|
|
461
|
+
uiMetadata,
|
|
462
|
+
bearerToken,
|
|
463
|
+
);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
log.error({ err, runId: run.id }, 'Failed to deliver approval prompt for channel run');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
lastStatus = current.status;
|
|
472
|
+
|
|
473
|
+
if (current.status === 'completed' || current.status === 'failed') {
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
channelDeliveryStore.markProcessed(eventId);
|
|
479
|
+
|
|
480
|
+
// Deliver the final assistant reply
|
|
481
|
+
await deliverReplyViaCallback(conversationId, externalChatId, replyCallbackUrl, bearerToken);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
log.error({ err, conversationId }, 'Approval-aware channel message processing failed');
|
|
484
|
+
channelDeliveryStore.recordProcessingFailure(eventId, err);
|
|
485
|
+
}
|
|
486
|
+
})();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// Approval interception
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
interface ApprovalInterceptionParams {
|
|
494
|
+
conversationId: string;
|
|
495
|
+
callbackData?: string;
|
|
496
|
+
content: string;
|
|
497
|
+
externalChatId: string;
|
|
498
|
+
replyCallbackUrl: string;
|
|
499
|
+
bearerToken?: string;
|
|
500
|
+
orchestrator: RunOrchestrator;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
interface ApprovalInterceptionResult {
|
|
504
|
+
handled: boolean;
|
|
505
|
+
type?: 'decision_applied' | 'reminder_sent';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Check for pending approvals and handle inbound messages accordingly.
|
|
510
|
+
*
|
|
511
|
+
* Returns `{ handled: true }` when the message was consumed by the approval
|
|
512
|
+
* flow (either as a decision or a reminder), so the caller should NOT proceed
|
|
513
|
+
* to normal message processing.
|
|
514
|
+
*/
|
|
515
|
+
async function handleApprovalInterception(
|
|
516
|
+
params: ApprovalInterceptionParams,
|
|
517
|
+
): Promise<ApprovalInterceptionResult> {
|
|
518
|
+
const {
|
|
519
|
+
conversationId,
|
|
520
|
+
callbackData,
|
|
521
|
+
content,
|
|
522
|
+
externalChatId,
|
|
523
|
+
replyCallbackUrl,
|
|
524
|
+
bearerToken,
|
|
525
|
+
orchestrator,
|
|
526
|
+
} = params;
|
|
527
|
+
|
|
528
|
+
const pendingPrompt = getChannelApprovalPrompt(conversationId);
|
|
529
|
+
if (!pendingPrompt) return { handled: false };
|
|
530
|
+
|
|
531
|
+
// Try to extract a decision from callback data (button press) first,
|
|
532
|
+
// then fall back to plain-text parsing.
|
|
533
|
+
let decision: ApprovalDecisionResult | null = null;
|
|
534
|
+
|
|
535
|
+
if (callbackData) {
|
|
536
|
+
decision = parseCallbackData(callbackData);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!decision && content) {
|
|
540
|
+
decision = parseApprovalDecision(content);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (decision) {
|
|
544
|
+
const result = handleChannelDecision(conversationId, decision, orchestrator);
|
|
545
|
+
|
|
546
|
+
if (result.applied) {
|
|
547
|
+
// Deliver the run's result back to the channel once the decision is applied.
|
|
548
|
+
// The run will resume in the background; deliver whatever assistant reply
|
|
549
|
+
// is available now (there may not be one yet if the run is still processing).
|
|
550
|
+
try {
|
|
551
|
+
await deliverReplyViaCallback(conversationId, externalChatId, replyCallbackUrl, bearerToken);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
log.error({ err, conversationId }, 'Failed to deliver post-decision reply');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return { handled: true, type: 'decision_applied' };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// The message is not a decision — send a reminder with the approval buttons.
|
|
561
|
+
const reminder = buildReminderPrompt(pendingPrompt);
|
|
562
|
+
const pending = getPendingConfirmationsByConversation(conversationId);
|
|
563
|
+
if (pending.length > 0) {
|
|
564
|
+
const uiMetadata = buildApprovalUIMetadata(reminder, pending[0]);
|
|
565
|
+
try {
|
|
566
|
+
await deliverApprovalPrompt(
|
|
567
|
+
replyCallbackUrl,
|
|
568
|
+
externalChatId,
|
|
569
|
+
reminder.promptText,
|
|
570
|
+
uiMetadata,
|
|
571
|
+
bearerToken,
|
|
572
|
+
);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
log.error({ err, conversationId }, 'Failed to deliver approval reminder');
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { handled: true, type: 'reminder_sent' };
|
|
579
|
+
}
|
|
580
|
+
|
|
301
581
|
async function deliverReplyViaCallback(
|
|
302
582
|
conversationId: string,
|
|
303
583
|
externalChatId: string,
|
|
@@ -46,19 +46,26 @@ export function handleListMessages(
|
|
|
46
46
|
url: URL,
|
|
47
47
|
interfacesDir: string | null,
|
|
48
48
|
): Response {
|
|
49
|
+
const conversationId = url.searchParams.get('conversationId');
|
|
49
50
|
const conversationKey = url.searchParams.get('conversationKey');
|
|
50
|
-
|
|
51
|
+
|
|
52
|
+
let resolvedConversationId: string | undefined;
|
|
53
|
+
if (conversationId) {
|
|
54
|
+
resolvedConversationId = conversationId;
|
|
55
|
+
} else if (conversationKey) {
|
|
56
|
+
const mapping = getConversationByKey(conversationKey);
|
|
57
|
+
resolvedConversationId = mapping?.conversationId;
|
|
58
|
+
} else {
|
|
51
59
|
return Response.json(
|
|
52
|
-
{ error: 'conversationKey query parameter is required' },
|
|
60
|
+
{ error: 'conversationKey or conversationId query parameter is required' },
|
|
53
61
|
{ status: 400 },
|
|
54
62
|
);
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
if (!mapping) {
|
|
65
|
+
if (!resolvedConversationId) {
|
|
59
66
|
return Response.json({ messages: [] });
|
|
60
67
|
}
|
|
61
|
-
const rawMessages = conversationStore.getMessages(
|
|
68
|
+
const rawMessages = conversationStore.getMessages(resolvedConversationId);
|
|
62
69
|
|
|
63
70
|
// Parse content blocks and extract text + tool calls
|
|
64
71
|
const parsed = rawMessages.map((msg) => {
|