@vellumai/assistant 0.3.3 → 0.3.4
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 +8 -16
- package/package.json +1 -1
- package/src/__tests__/call-orchestrator.test.ts +321 -0
- package/src/__tests__/channel-approval-routes.test.ts +382 -124
- package/src/__tests__/channel-approvals.test.ts +51 -2
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +187 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/daemon-lifecycle.test.ts +635 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +73 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/calls/call-orchestrator.ts +63 -11
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/messaging/SKILL.md +4 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +3 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +16 -0
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +52 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +38 -0
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +36 -13
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/ipc-contract.ts +6 -0
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +105 -502
- package/src/daemon/session-agent-loop.ts +5 -14
- package/src/daemon/session-runtime-assembly.ts +60 -44
- package/src/daemon/session.ts +8 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1019 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-worker.ts +7 -1
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +30 -1
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +1 -0
- package/src/memory/search/types.ts +2 -0
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/channel-approvals.ts +17 -3
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +15 -4
- package/src/runtime/routes/channel-routes.ts +172 -84
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +8 -1
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/executor.ts +10 -2
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/util/platform.ts +8 -1
- package/src/util/retry.ts +4 -4
|
@@ -127,12 +127,21 @@ function effectivePromptText(
|
|
|
127
127
|
return plainTextFallback;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Build contextual deny guidance for guardian-gated auto-deny paths.
|
|
132
|
+
* This is passed through the confirmation pipeline so the assistant can
|
|
133
|
+
* produce a single, user-facing message with next steps.
|
|
134
|
+
*/
|
|
135
|
+
function buildGuardianDenyContext(
|
|
136
|
+
toolName: string,
|
|
137
|
+
denialReason: DenialReason,
|
|
138
|
+
sourceChannel: string,
|
|
139
|
+
): string {
|
|
140
|
+
if (denialReason === 'no_identity') {
|
|
141
|
+
return `Permission denied: the action "${toolName}" requires guardian approval, but your identity could not be verified on ${sourceChannel}. Do not retry yet. Explain this clearly, ask the user to message from a verifiable direct account/chat, and then retry after identity is available.`;
|
|
142
|
+
}
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
return process.env.CHANNEL_APPROVALS_ENABLED === 'true';
|
|
144
|
+
return `Permission denied: the action "${toolName}" requires guardian approval, but no guardian is configured for this ${sourceChannel} channel. Do not retry yet. Explain that a guardian must be set up first. The guardian/admin should open the Channels section in Settings and click "Verify Guardian", or ask the assistant to set up guardian verification. The setup flow will provide a verification token to send as /guardian_verify <token> in the ${sourceChannel} chat.`;
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
// ---------------------------------------------------------------------------
|
|
@@ -169,13 +178,22 @@ export async function handleDeleteConversation(req: Request, assistantId: string
|
|
|
169
178
|
return Response.json({ error: 'externalChatId is required' }, { status: 400 });
|
|
170
179
|
}
|
|
171
180
|
|
|
172
|
-
// Delete
|
|
173
|
-
//
|
|
181
|
+
// Delete the assistant-scoped key unconditionally. The legacy key is
|
|
182
|
+
// canonical for the self assistant and must not be deleted from non-self
|
|
183
|
+
// routes, otherwise a non-self reset can accidentally reset self state.
|
|
174
184
|
const legacyKey = `${sourceChannel}:${externalChatId}`;
|
|
175
185
|
const scopedKey = `asst:${assistantId}:${sourceChannel}:${externalChatId}`;
|
|
176
|
-
deleteConversationKey(legacyKey);
|
|
177
186
|
deleteConversationKey(scopedKey);
|
|
178
|
-
|
|
187
|
+
if (assistantId === 'self') {
|
|
188
|
+
deleteConversationKey(legacyKey);
|
|
189
|
+
}
|
|
190
|
+
// external_conversation_bindings is currently assistant-agnostic
|
|
191
|
+
// (unique by sourceChannel + externalChatId). Restrict mutations to the
|
|
192
|
+
// canonical self-assistant route so multi-assistant legacy routes do not
|
|
193
|
+
// clobber each other's bindings.
|
|
194
|
+
if (assistantId === 'self') {
|
|
195
|
+
externalConversationStore.deleteBindingByChannelChat(sourceChannel, externalChatId);
|
|
196
|
+
}
|
|
179
197
|
|
|
180
198
|
return Response.json({ ok: true });
|
|
181
199
|
}
|
|
@@ -338,15 +356,19 @@ export async function handleChannelInbound(
|
|
|
338
356
|
{ sourceMessageId, assistantId },
|
|
339
357
|
);
|
|
340
358
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
359
|
+
// external_conversation_bindings is assistant-agnostic. Restrict writes to
|
|
360
|
+
// self so assistant-scoped legacy routes do not overwrite each other's
|
|
361
|
+
// channel binding metadata for the same chat.
|
|
362
|
+
if (assistantId === 'self') {
|
|
363
|
+
externalConversationStore.upsertBinding({
|
|
364
|
+
conversationId: result.conversationId,
|
|
365
|
+
sourceChannel,
|
|
366
|
+
externalChatId,
|
|
367
|
+
externalUserId: body.senderExternalUserId ?? null,
|
|
368
|
+
displayName: body.senderName ?? null,
|
|
369
|
+
username: body.senderUsername ?? null,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
350
372
|
|
|
351
373
|
const metadataHintsRaw = sourceMetadata?.hints;
|
|
352
374
|
const metadataHints = Array.isArray(metadataHintsRaw)
|
|
@@ -384,6 +406,7 @@ export async function handleChannelInbound(
|
|
|
384
406
|
await deliverChannelReply(replyCallbackUrl, {
|
|
385
407
|
chatId: externalChatId,
|
|
386
408
|
text: replyText,
|
|
409
|
+
assistantId,
|
|
387
410
|
}, bearerToken);
|
|
388
411
|
} catch (err) {
|
|
389
412
|
log.error({ err, externalChatId }, 'Failed to deliver guardian verification reply');
|
|
@@ -403,9 +426,7 @@ export async function handleChannelInbound(
|
|
|
403
426
|
// When a guardian binding exists, non-guardian actors get stricter
|
|
404
427
|
// side-effect controls and their approvals route to the guardian's chat.
|
|
405
428
|
//
|
|
406
|
-
// Guardian
|
|
407
|
-
// The approval flag only gates the interactive approval prompting UX;
|
|
408
|
-
// actor-role resolution and fail-closed denial are always active.
|
|
429
|
+
// Guardian actor-role resolution always runs.
|
|
409
430
|
let guardianCtx: GuardianContext = { actorRole: 'guardian' };
|
|
410
431
|
if (body.senderExternalUserId) {
|
|
411
432
|
const senderIsGuardian = isGuardian(assistantId, sourceChannel, body.senderExternalUserId);
|
|
@@ -435,23 +456,20 @@ export async function handleChannelInbound(
|
|
|
435
456
|
}
|
|
436
457
|
}
|
|
437
458
|
} else {
|
|
438
|
-
// No sender identity available —
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
requesterChatId: externalChatId,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
459
|
+
// No sender identity available — treat as unverified and fail closed.
|
|
460
|
+
// Multi-actor channels must not grant default guardian permissions when
|
|
461
|
+
// the inbound actor cannot be identified.
|
|
462
|
+
guardianCtx = {
|
|
463
|
+
actorRole: 'unverified_channel',
|
|
464
|
+
denialReason: 'no_identity',
|
|
465
|
+
requesterExternalUserId: undefined,
|
|
466
|
+
requesterChatId: externalChatId,
|
|
467
|
+
};
|
|
450
468
|
}
|
|
451
469
|
|
|
452
|
-
// ── Approval interception
|
|
470
|
+
// ── Approval interception ──
|
|
471
|
+
// Keep this active whenever orchestrator + callback context are available.
|
|
453
472
|
if (
|
|
454
|
-
isChannelApprovalsEnabled() &&
|
|
455
473
|
runOrchestrator &&
|
|
456
474
|
replyCallbackUrl &&
|
|
457
475
|
!result.duplicate
|
|
@@ -507,6 +525,7 @@ export async function handleChannelInbound(
|
|
|
507
525
|
senderExternalUserId: body.senderExternalUserId,
|
|
508
526
|
senderUsername: body.senderUsername,
|
|
509
527
|
replyCallbackUrl,
|
|
528
|
+
assistantId,
|
|
510
529
|
});
|
|
511
530
|
|
|
512
531
|
const contentToCheck = content ?? '';
|
|
@@ -522,13 +541,15 @@ export async function handleChannelInbound(
|
|
|
522
541
|
throw new IngressBlockedError(ingressCheck.userNotice!, ingressCheck.detectedTypes);
|
|
523
542
|
}
|
|
524
543
|
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
const useApprovalPath =
|
|
529
|
-
|
|
544
|
+
// Use the approval-aware orchestrator path whenever orchestration and a
|
|
545
|
+
// callback delivery target are available. This keeps approval handling
|
|
546
|
+
// consistent across all channels and avoids silent prompt timeouts.
|
|
547
|
+
const useApprovalPath = Boolean(
|
|
548
|
+
runOrchestrator &&
|
|
549
|
+
replyCallbackUrl,
|
|
550
|
+
);
|
|
530
551
|
|
|
531
|
-
if (useApprovalPath) {
|
|
552
|
+
if (useApprovalPath && runOrchestrator && replyCallbackUrl) {
|
|
532
553
|
processChannelMessageWithApprovals({
|
|
533
554
|
orchestrator: runOrchestrator,
|
|
534
555
|
conversationId: result.conversationId,
|
|
@@ -557,6 +578,7 @@ export async function handleChannelInbound(
|
|
|
557
578
|
metadataUxBrief,
|
|
558
579
|
replyCallbackUrl,
|
|
559
580
|
bearerToken,
|
|
581
|
+
assistantId,
|
|
560
582
|
});
|
|
561
583
|
}
|
|
562
584
|
}
|
|
@@ -580,6 +602,7 @@ interface BackgroundProcessingParams {
|
|
|
580
602
|
metadataUxBrief?: string;
|
|
581
603
|
replyCallbackUrl?: string;
|
|
582
604
|
bearerToken?: string;
|
|
605
|
+
assistantId?: string;
|
|
583
606
|
}
|
|
584
607
|
|
|
585
608
|
function processChannelMessageInBackground(params: BackgroundProcessingParams): void {
|
|
@@ -595,6 +618,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
595
618
|
metadataUxBrief,
|
|
596
619
|
replyCallbackUrl,
|
|
597
620
|
bearerToken,
|
|
621
|
+
assistantId,
|
|
598
622
|
} = params;
|
|
599
623
|
|
|
600
624
|
(async () => {
|
|
@@ -616,7 +640,13 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
616
640
|
channelDeliveryStore.markProcessed(eventId);
|
|
617
641
|
|
|
618
642
|
if (replyCallbackUrl) {
|
|
619
|
-
await deliverReplyViaCallback(
|
|
643
|
+
await deliverReplyViaCallback(
|
|
644
|
+
conversationId,
|
|
645
|
+
externalChatId,
|
|
646
|
+
replyCallbackUrl,
|
|
647
|
+
bearerToken,
|
|
648
|
+
assistantId,
|
|
649
|
+
);
|
|
620
650
|
}
|
|
621
651
|
} catch (err) {
|
|
622
652
|
log.error({ err, conversationId }, 'Background channel message processing failed');
|
|
@@ -714,6 +744,14 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
|
|
|
714
744
|
const startTime = Date.now();
|
|
715
745
|
const pollMaxWait = getEffectivePollMaxWait();
|
|
716
746
|
let lastStatus = run.status;
|
|
747
|
+
// Track whether a post-decision delivery path is guaranteed for this
|
|
748
|
+
// run. Set to true only when the approval prompt is successfully
|
|
749
|
+
// delivered (guardian or standard path), meaning
|
|
750
|
+
// handleApprovalInterception will schedule schedulePostDecisionDelivery
|
|
751
|
+
// when a decision arrives. Auto-deny paths (unverified channel, prompt
|
|
752
|
+
// delivery failures) do NOT set this flag because no post-decision
|
|
753
|
+
// delivery is scheduled in those cases.
|
|
754
|
+
let hasPostDecisionDelivery = false;
|
|
717
755
|
|
|
718
756
|
while (Date.now() - startTime < pollMaxWait) {
|
|
719
757
|
await new Promise((resolve) => setTimeout(resolve, RUN_POLL_INTERVAL_MS));
|
|
@@ -726,18 +764,16 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
|
|
|
726
764
|
|
|
727
765
|
if (isUnverifiedChannel && pending.length > 0) {
|
|
728
766
|
// Unverified channel — auto-deny the sensitive action (fail-closed).
|
|
729
|
-
handleChannelDecision(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
log.error({ err, runId: run.id }, 'Failed to deliver unverified-channel denial notice');
|
|
740
|
-
}
|
|
767
|
+
handleChannelDecision(
|
|
768
|
+
conversationId,
|
|
769
|
+
{ action: 'reject', source: 'plain_text' },
|
|
770
|
+
orchestrator,
|
|
771
|
+
buildGuardianDenyContext(
|
|
772
|
+
pending[0].toolName,
|
|
773
|
+
guardianCtx.denialReason ?? 'no_binding',
|
|
774
|
+
sourceChannel,
|
|
775
|
+
),
|
|
776
|
+
);
|
|
741
777
|
} else if (isNonGuardian && guardianCtx.guardianChatId && pending.length > 0) {
|
|
742
778
|
// Non-guardian actor: route the approval prompt to the guardian's chat
|
|
743
779
|
const guardianPrompt = buildGuardianApprovalPrompt(
|
|
@@ -774,9 +810,11 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
|
|
|
774
810
|
guardianCtx.guardianChatId,
|
|
775
811
|
guardianText,
|
|
776
812
|
uiMetadata,
|
|
813
|
+
assistantId,
|
|
777
814
|
bearerToken,
|
|
778
815
|
);
|
|
779
816
|
guardianNotified = true;
|
|
817
|
+
hasPostDecisionDelivery = true;
|
|
780
818
|
} catch (err) {
|
|
781
819
|
log.error({ err, runId: run.id }, 'Failed to deliver guardian approval prompt');
|
|
782
820
|
// Deny the approval and the underlying run — fail-closed. If
|
|
@@ -789,6 +827,7 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
|
|
|
789
827
|
await deliverChannelReply(replyCallbackUrl, {
|
|
790
828
|
chatId: guardianCtx.requesterChatId ?? externalChatId,
|
|
791
829
|
text: `Your request to run "${pending[0].toolName}" could not be sent to the guardian for approval. The action has been denied.`,
|
|
830
|
+
assistantId,
|
|
792
831
|
}, bearerToken);
|
|
793
832
|
} catch (notifyErr) {
|
|
794
833
|
log.error({ err: notifyErr, runId: run.id }, 'Failed to notify requester of guardian delivery failure');
|
|
@@ -801,6 +840,7 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
|
|
|
801
840
|
await deliverChannelReply(replyCallbackUrl, {
|
|
802
841
|
chatId: guardianCtx.requesterChatId ?? externalChatId,
|
|
803
842
|
text: `Your request to run "${pending[0].toolName}" has been sent to the guardian for approval.`,
|
|
843
|
+
assistantId,
|
|
804
844
|
}, bearerToken);
|
|
805
845
|
} catch (err) {
|
|
806
846
|
log.error({ err, runId: run.id }, 'Failed to notify requester of pending guardian approval');
|
|
@@ -823,8 +863,10 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
|
|
|
823
863
|
externalChatId,
|
|
824
864
|
promptTextForChannel,
|
|
825
865
|
uiMetadata,
|
|
866
|
+
assistantId,
|
|
826
867
|
bearerToken,
|
|
827
868
|
);
|
|
869
|
+
hasPostDecisionDelivery = true;
|
|
828
870
|
} catch (err) {
|
|
829
871
|
// Fail-closed: if we cannot deliver the approval prompt, the
|
|
830
872
|
// user will never see it and the run would hang indefinitely
|
|
@@ -867,7 +909,13 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
|
|
|
867
909
|
// rather than permanently losing the reply.
|
|
868
910
|
if (channelDeliveryStore.claimRunDelivery(run.id)) {
|
|
869
911
|
try {
|
|
870
|
-
await deliverReplyViaCallback(
|
|
912
|
+
await deliverReplyViaCallback(
|
|
913
|
+
conversationId,
|
|
914
|
+
externalChatId,
|
|
915
|
+
replyCallbackUrl,
|
|
916
|
+
bearerToken,
|
|
917
|
+
assistantId,
|
|
918
|
+
);
|
|
871
919
|
} catch (deliveryErr) {
|
|
872
920
|
channelDeliveryStore.resetRunDeliveryClaim(run.id);
|
|
873
921
|
throw deliveryErr;
|
|
@@ -884,23 +932,39 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
|
|
|
884
932
|
updateApprovalDecision(approvalReq.id, { status: outcomeStatus });
|
|
885
933
|
}
|
|
886
934
|
}
|
|
887
|
-
} else if (
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
935
|
+
} else if (
|
|
936
|
+
finalRun?.status === 'needs_confirmation' ||
|
|
937
|
+
(hasPostDecisionDelivery && finalRun?.status === 'running')
|
|
938
|
+
) {
|
|
939
|
+
// The run is either still waiting for an approval decision or was
|
|
940
|
+
// recently approved and has resumed execution. In both cases, mark
|
|
941
|
+
// the event as processed rather than failed:
|
|
942
|
+
//
|
|
943
|
+
// - needs_confirmation: the run will resume when the user clicks
|
|
944
|
+
// approve/reject, and `handleApprovalInterception` will deliver
|
|
945
|
+
// the reply via `schedulePostDecisionDelivery`.
|
|
946
|
+
//
|
|
947
|
+
// - running (after successful prompt delivery): an approval was
|
|
948
|
+
// applied near the poll deadline and the run resumed but hasn't
|
|
949
|
+
// reached terminal state yet. `handleApprovalInterception` has
|
|
950
|
+
// already scheduled post-decision delivery, so the final reply
|
|
951
|
+
// will be delivered. This condition is only true when the approval
|
|
952
|
+
// prompt was actually delivered (not in auto-deny paths), ensuring
|
|
953
|
+
// we don't suppress retry/dead-letter for cases where no
|
|
954
|
+
// post-decision delivery path exists.
|
|
955
|
+
//
|
|
956
|
+
// Marking either state as failed would cause the generic retry sweep
|
|
957
|
+
// to replay through `processMessage`, which throws "Session is
|
|
894
958
|
// already processing a message" and dead-letters a valid conversation.
|
|
895
959
|
log.warn(
|
|
896
|
-
{ runId: run.id, status: finalRun.status, conversationId },
|
|
897
|
-
'Approval-path poll loop timed out while run
|
|
960
|
+
{ runId: run.id, status: finalRun.status, conversationId, hasPostDecisionDelivery },
|
|
961
|
+
'Approval-path poll loop timed out while run is in approval-related state; marking event as processed',
|
|
898
962
|
);
|
|
899
963
|
channelDeliveryStore.markProcessed(eventId);
|
|
900
964
|
} else {
|
|
901
|
-
// The run is in a non-terminal, non-approval state (e.g. running
|
|
902
|
-
// needs_secret, or disappeared). Record a
|
|
903
|
-
// retry/dead-letter machinery can handle it.
|
|
965
|
+
// The run is in a non-terminal, non-approval state (e.g. running
|
|
966
|
+
// without prior approval, needs_secret, or disappeared). Record a
|
|
967
|
+
// processing failure so the retry/dead-letter machinery can handle it.
|
|
904
968
|
const timeoutErr = new Error(
|
|
905
969
|
`Approval poll timeout: run did not reach terminal state within ${pollMaxWait}ms (status: ${finalRun?.status ?? 'null'})`,
|
|
906
970
|
);
|
|
@@ -1003,6 +1067,7 @@ async function handleApprovalInterception(
|
|
|
1003
1067
|
await deliverChannelReply(replyCallbackUrl, {
|
|
1004
1068
|
chatId: externalChatId,
|
|
1005
1069
|
text: `You have ${allPending.length} pending approval requests. Please use the approval buttons to respond to a specific request.`,
|
|
1070
|
+
assistantId,
|
|
1006
1071
|
}, bearerToken);
|
|
1007
1072
|
} catch (err) {
|
|
1008
1073
|
log.error({ err, externalChatId }, 'Failed to deliver disambiguation notice');
|
|
@@ -1035,6 +1100,7 @@ async function handleApprovalInterception(
|
|
|
1035
1100
|
await deliverChannelReply(replyCallbackUrl, {
|
|
1036
1101
|
chatId: externalChatId,
|
|
1037
1102
|
text: 'Only the verified guardian can approve or deny this request.',
|
|
1103
|
+
assistantId,
|
|
1038
1104
|
}, bearerToken);
|
|
1039
1105
|
} catch (err) {
|
|
1040
1106
|
log.error({ err, externalChatId }, 'Failed to deliver guardian identity rejection notice');
|
|
@@ -1075,6 +1141,7 @@ async function handleApprovalInterception(
|
|
|
1075
1141
|
await deliverChannelReply(replyCallbackUrl, {
|
|
1076
1142
|
chatId: guardianApproval.requesterChatId,
|
|
1077
1143
|
text: outcomeText,
|
|
1144
|
+
assistantId,
|
|
1078
1145
|
}, bearerToken);
|
|
1079
1146
|
} catch (err) {
|
|
1080
1147
|
log.error({ err, conversationId: guardianApproval.conversationId }, 'Failed to notify requester of guardian decision');
|
|
@@ -1090,6 +1157,7 @@ async function handleApprovalInterception(
|
|
|
1090
1157
|
guardianApproval.requesterChatId,
|
|
1091
1158
|
replyCallbackUrl,
|
|
1092
1159
|
bearerToken,
|
|
1160
|
+
assistantId,
|
|
1093
1161
|
);
|
|
1094
1162
|
}
|
|
1095
1163
|
}
|
|
@@ -1117,6 +1185,7 @@ async function handleApprovalInterception(
|
|
|
1117
1185
|
externalChatId,
|
|
1118
1186
|
reminderText,
|
|
1119
1187
|
uiMetadata,
|
|
1188
|
+
assistantId,
|
|
1120
1189
|
bearerToken,
|
|
1121
1190
|
);
|
|
1122
1191
|
} catch (err) {
|
|
@@ -1126,11 +1195,6 @@ async function handleApprovalInterception(
|
|
|
1126
1195
|
|
|
1127
1196
|
return { handled: true, type: 'reminder_sent' };
|
|
1128
1197
|
}
|
|
1129
|
-
|
|
1130
|
-
// Callback with a run ID that no longer has a pending approval — stale button
|
|
1131
|
-
if (decision?.runId) {
|
|
1132
|
-
return { handled: true, type: 'stale_ignored' };
|
|
1133
|
-
}
|
|
1134
1198
|
}
|
|
1135
1199
|
|
|
1136
1200
|
// ── Standard approval interception (existing flow) ──
|
|
@@ -1142,17 +1206,26 @@ async function handleApprovalInterception(
|
|
|
1142
1206
|
if (guardianCtx.actorRole === 'unverified_channel') {
|
|
1143
1207
|
const pending = getPendingConfirmationsByConversation(conversationId);
|
|
1144
1208
|
if (pending.length > 0) {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1209
|
+
const denyResult = handleChannelDecision(
|
|
1210
|
+
conversationId,
|
|
1211
|
+
{ action: 'reject', source: 'plain_text' },
|
|
1212
|
+
orchestrator,
|
|
1213
|
+
buildGuardianDenyContext(
|
|
1214
|
+
pending[0].toolName,
|
|
1215
|
+
guardianCtx.denialReason ?? 'no_binding',
|
|
1216
|
+
sourceChannel,
|
|
1217
|
+
),
|
|
1218
|
+
);
|
|
1219
|
+
if (denyResult.applied && denyResult.runId) {
|
|
1220
|
+
schedulePostDecisionDelivery(
|
|
1221
|
+
orchestrator,
|
|
1222
|
+
denyResult.runId,
|
|
1223
|
+
conversationId,
|
|
1224
|
+
externalChatId,
|
|
1225
|
+
replyCallbackUrl,
|
|
1226
|
+
bearerToken,
|
|
1227
|
+
assistantId,
|
|
1228
|
+
);
|
|
1156
1229
|
}
|
|
1157
1230
|
return { handled: true, type: 'decision_applied' };
|
|
1158
1231
|
}
|
|
@@ -1170,6 +1243,7 @@ async function handleApprovalInterception(
|
|
|
1170
1243
|
await deliverChannelReply(replyCallbackUrl, {
|
|
1171
1244
|
chatId: externalChatId,
|
|
1172
1245
|
text: 'Your request is pending guardian approval. Only the verified guardian can approve or deny this request.',
|
|
1246
|
+
assistantId,
|
|
1173
1247
|
}, bearerToken);
|
|
1174
1248
|
} catch (err) {
|
|
1175
1249
|
log.error({ err, conversationId }, 'Failed to deliver guardian-pending notice to requester');
|
|
@@ -1196,6 +1270,7 @@ async function handleApprovalInterception(
|
|
|
1196
1270
|
await deliverChannelReply(replyCallbackUrl, {
|
|
1197
1271
|
chatId: externalChatId,
|
|
1198
1272
|
text: 'Your guardian approval request has expired and the action has been denied. Please try again.',
|
|
1273
|
+
assistantId,
|
|
1199
1274
|
}, bearerToken);
|
|
1200
1275
|
} catch (err) {
|
|
1201
1276
|
log.error({ err, conversationId }, 'Failed to deliver guardian-expiry notice to requester');
|
|
@@ -1247,6 +1322,7 @@ async function handleApprovalInterception(
|
|
|
1247
1322
|
externalChatId,
|
|
1248
1323
|
replyCallbackUrl,
|
|
1249
1324
|
bearerToken,
|
|
1325
|
+
assistantId,
|
|
1250
1326
|
);
|
|
1251
1327
|
}
|
|
1252
1328
|
|
|
@@ -1269,6 +1345,7 @@ async function handleApprovalInterception(
|
|
|
1269
1345
|
externalChatId,
|
|
1270
1346
|
reminderText,
|
|
1271
1347
|
uiMetadata,
|
|
1348
|
+
assistantId,
|
|
1272
1349
|
bearerToken,
|
|
1273
1350
|
);
|
|
1274
1351
|
} catch (err) {
|
|
@@ -1296,6 +1373,7 @@ function schedulePostDecisionDelivery(
|
|
|
1296
1373
|
externalChatId: string,
|
|
1297
1374
|
replyCallbackUrl: string,
|
|
1298
1375
|
bearerToken?: string,
|
|
1376
|
+
assistantId?: string,
|
|
1299
1377
|
): void {
|
|
1300
1378
|
(async () => {
|
|
1301
1379
|
try {
|
|
@@ -1307,7 +1385,13 @@ function schedulePostDecisionDelivery(
|
|
|
1307
1385
|
if (current.status === 'completed' || current.status === 'failed') {
|
|
1308
1386
|
if (channelDeliveryStore.claimRunDelivery(runId)) {
|
|
1309
1387
|
try {
|
|
1310
|
-
await deliverReplyViaCallback(
|
|
1388
|
+
await deliverReplyViaCallback(
|
|
1389
|
+
conversationId,
|
|
1390
|
+
externalChatId,
|
|
1391
|
+
replyCallbackUrl,
|
|
1392
|
+
bearerToken,
|
|
1393
|
+
assistantId,
|
|
1394
|
+
);
|
|
1311
1395
|
} catch (deliveryErr) {
|
|
1312
1396
|
channelDeliveryStore.resetRunDeliveryClaim(runId);
|
|
1313
1397
|
throw deliveryErr;
|
|
@@ -1331,6 +1415,7 @@ async function deliverReplyViaCallback(
|
|
|
1331
1415
|
externalChatId: string,
|
|
1332
1416
|
callbackUrl: string,
|
|
1333
1417
|
bearerToken?: string,
|
|
1418
|
+
assistantId?: string,
|
|
1334
1419
|
): Promise<void> {
|
|
1335
1420
|
const msgs = conversationStore.getMessages(conversationId);
|
|
1336
1421
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
@@ -1353,6 +1438,7 @@ async function deliverReplyViaCallback(
|
|
|
1353
1438
|
chatId: externalChatId,
|
|
1354
1439
|
text: rendered.text || undefined,
|
|
1355
1440
|
attachments: replyAttachments.length > 0 ? replyAttachments : undefined,
|
|
1441
|
+
assistantId,
|
|
1356
1442
|
}, bearerToken);
|
|
1357
1443
|
}
|
|
1358
1444
|
break;
|
|
@@ -1453,6 +1539,7 @@ export function sweepExpiredGuardianApprovals(
|
|
|
1453
1539
|
deliverChannelReply(deliverUrl, {
|
|
1454
1540
|
chatId: approval.requesterChatId,
|
|
1455
1541
|
text: `Your guardian approval request for "${approval.toolName}" has expired and the action has been denied. Please try again.`,
|
|
1542
|
+
assistantId: approval.assistantId,
|
|
1456
1543
|
}, bearerToken).catch((err) => {
|
|
1457
1544
|
log.error({ err, runId: approval.runId }, 'Failed to notify requester of guardian approval expiry');
|
|
1458
1545
|
});
|
|
@@ -1461,6 +1548,7 @@ export function sweepExpiredGuardianApprovals(
|
|
|
1461
1548
|
deliverChannelReply(deliverUrl, {
|
|
1462
1549
|
chatId: approval.guardianChatId,
|
|
1463
1550
|
text: `The approval request for "${approval.toolName}" from user ${approval.requesterExternalUserId} has expired and was automatically denied.`,
|
|
1551
|
+
assistantId: approval.assistantId,
|
|
1464
1552
|
}, bearerToken).catch((err) => {
|
|
1465
1553
|
log.error({ err, runId: approval.runId }, 'Failed to notify guardian of approval expiry');
|
|
1466
1554
|
});
|
|
@@ -5,6 +5,7 @@ import { getOrCreateConversation } from '../../memory/conversation-key-store.js'
|
|
|
5
5
|
import * as attachmentsStore from '../../memory/attachments-store.js';
|
|
6
6
|
import * as runsStore from '../../memory/runs-store.js';
|
|
7
7
|
import { addRule } from '../../permissions/trust-store.js';
|
|
8
|
+
import { getTool } from '../../tools/registry.js';
|
|
8
9
|
import { getLogger } from '../../util/logger.js';
|
|
9
10
|
import type { RunOrchestrator } from '../run-orchestrator.js';
|
|
10
11
|
|
|
@@ -200,8 +201,13 @@ export async function handleAddTrustRule(
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
try {
|
|
204
|
+
// Only persist executionTarget for skill-origin tools — core tools don't
|
|
205
|
+
// set it in their PolicyContext, so a persisted value would prevent the
|
|
206
|
+
// rule from ever matching on subsequent permission checks.
|
|
207
|
+
const tool = getTool(confirmation.toolName);
|
|
208
|
+
const executionTarget = tool?.origin === 'skill' ? confirmation.executionTarget : undefined;
|
|
203
209
|
addRule(confirmation.toolName, pattern, scope, decision, undefined, {
|
|
204
|
-
executionTarget
|
|
210
|
+
executionTarget,
|
|
205
211
|
});
|
|
206
212
|
log.info(
|
|
207
213
|
{ tool: confirmation.toolName, pattern, scope, decision, runId },
|
|
@@ -251,13 +251,20 @@ export class RunOrchestrator {
|
|
|
251
251
|
* - `'run_not_found'` – no run exists with the given ID
|
|
252
252
|
* - `'no_pending_decision'` – run exists but is not awaiting a confirmation
|
|
253
253
|
*/
|
|
254
|
-
submitDecision(
|
|
254
|
+
submitDecision(
|
|
255
|
+
runId: string,
|
|
256
|
+
decision: UserDecision,
|
|
257
|
+
decisionContext?: string,
|
|
258
|
+
): 'applied' | 'run_not_found' | 'no_pending_decision' {
|
|
255
259
|
const pendingState = this.pending.get(runId);
|
|
256
260
|
if (pendingState) {
|
|
257
261
|
runsStore.clearRunConfirmation(runId);
|
|
258
262
|
pendingState.session.handleConfirmationResponse(
|
|
259
263
|
pendingState.prompterRequestId,
|
|
260
264
|
decision,
|
|
265
|
+
undefined,
|
|
266
|
+
undefined,
|
|
267
|
+
decisionContext,
|
|
261
268
|
);
|
|
262
269
|
this.pending.delete(runId);
|
|
263
270
|
return 'applied';
|