@vellumai/assistant 0.3.26 → 0.3.28
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/ARCHITECTURE.md +48 -1
- package/Dockerfile +2 -2
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +6 -2
- package/src/__tests__/agent-loop.test.ts +119 -0
- package/src/__tests__/bundled-asset.test.ts +107 -0
- package/src/__tests__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +19 -19
- package/src/__tests__/guardian-routing-invariants.test.ts +954 -0
- package/src/__tests__/mcp-cli.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +31 -29
- package/src/__tests__/notification-decision-fallback.test.ts +61 -3
- package/src/__tests__/notification-decision-strategy.test.ts +17 -0
- package/src/__tests__/notification-guardian-path.test.ts +13 -15
- package/src/__tests__/onboarding-template-contract.test.ts +116 -21
- package/src/__tests__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
- package/src/__tests__/session-runtime-assembly.test.ts +76 -47
- package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
- package/src/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +147 -2
- package/src/calls/types.ts +1 -1
- package/src/config/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +22 -17
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/lifecycle.ts +3 -16
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +32 -5
- package/src/daemon/session-process.ts +68 -307
- package/src/daemon/session-runtime-assembly.ts +112 -24
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +1 -0
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +524 -0
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/db-init.ts +16 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +52 -0
- package/src/notifications/copy-composer.ts +16 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +137 -0
- package/src/runtime/actor-trust-resolver.ts +225 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- package/src/runtime/guardian-context-resolver.ts +32 -7
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +687 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- package/src/runtime/routes/conversation-routes.ts +18 -0
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/inbound-message-handler.ts +170 -525
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +44 -1
- package/src/tools/types.ts +11 -0
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
|
@@ -67,6 +67,11 @@ mock.module('../memory/ingress-member-store.js', () => ({
|
|
|
67
67
|
updateLastSeen: () => {},
|
|
68
68
|
}));
|
|
69
69
|
import type { Session } from '../daemon/session.js';
|
|
70
|
+
import {
|
|
71
|
+
createCanonicalGuardianDelivery,
|
|
72
|
+
createCanonicalGuardianRequest,
|
|
73
|
+
getCanonicalGuardianRequest,
|
|
74
|
+
} from '../memory/canonical-guardian-store.js';
|
|
70
75
|
import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
|
|
71
76
|
import {
|
|
72
77
|
createApprovalRequest,
|
|
@@ -110,6 +115,9 @@ function ensureConversation(conversationId: string): void {
|
|
|
110
115
|
|
|
111
116
|
function resetTables(): void {
|
|
112
117
|
const db = getDb();
|
|
118
|
+
db.run('DELETE FROM scoped_approval_grants');
|
|
119
|
+
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
120
|
+
db.run('DELETE FROM canonical_guardian_requests');
|
|
113
121
|
db.run('DELETE FROM channel_guardian_approval_requests');
|
|
114
122
|
db.run('DELETE FROM channel_guardian_verification_challenges');
|
|
115
123
|
db.run('DELETE FROM channel_guardian_bindings');
|
|
@@ -458,7 +466,7 @@ describe('empty content with callbackData bypasses validation', () => {
|
|
|
458
466
|
const res = await handleChannelInbound(req, noopProcessMessage);
|
|
459
467
|
expect(res.status).toBe(400);
|
|
460
468
|
const body = await res.json() as Record<string, unknown>;
|
|
461
|
-
expect(body.error).toBe('content or attachmentIds is required');
|
|
469
|
+
expect((body.error as Record<string, unknown>).message).toBe('content or attachmentIds is required');
|
|
462
470
|
});
|
|
463
471
|
|
|
464
472
|
test('allows empty content when callbackData is present', async () => {
|
|
@@ -2645,4 +2653,169 @@ describe('background channel processing approval prompts', () => {
|
|
|
2645
2653
|
expect(processCalls.length).toBeGreaterThan(0);
|
|
2646
2654
|
expect(processCalls[0].options?.isInteractive).toBe(false);
|
|
2647
2655
|
});
|
|
2656
|
+
|
|
2657
|
+
test('unverified channel turns never broadcast approval prompts', async () => {
|
|
2658
|
+
// No guardian binding is created, so the sender resolves to unverified_channel.
|
|
2659
|
+
const deliverPromptSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
|
|
2660
|
+
const processCalls: Array<{ options?: Record<string, unknown> }> = [];
|
|
2661
|
+
|
|
2662
|
+
const processMessage = mock(async (
|
|
2663
|
+
conversationId: string,
|
|
2664
|
+
_content: string,
|
|
2665
|
+
_attachmentIds?: string[],
|
|
2666
|
+
options?: Record<string, unknown>,
|
|
2667
|
+
) => {
|
|
2668
|
+
processCalls.push({ options });
|
|
2669
|
+
|
|
2670
|
+
// Simulate a pending confirmation becoming visible while background
|
|
2671
|
+
// processing is running. Unverified actors must still not receive it.
|
|
2672
|
+
registerPendingInteraction('req-bg-unverified-1', conversationId, 'host_bash', {
|
|
2673
|
+
input: { command: 'ls -la' },
|
|
2674
|
+
riskLevel: 'medium',
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
2678
|
+
return { messageId: 'msg-bg-unverified-1' };
|
|
2679
|
+
});
|
|
2680
|
+
|
|
2681
|
+
const req = makeInboundRequest({
|
|
2682
|
+
content: 'run ls',
|
|
2683
|
+
sourceChannel: 'telegram',
|
|
2684
|
+
replyCallbackUrl: 'https://gateway.test/deliver/telegram',
|
|
2685
|
+
externalMessageId: 'msg-bg-unverified-1',
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
const res = await handleChannelInbound(req, processMessage as unknown as typeof noopProcessMessage, 'token');
|
|
2689
|
+
const body = await res.json() as Record<string, unknown>;
|
|
2690
|
+
expect(body.accepted).toBe(true);
|
|
2691
|
+
|
|
2692
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
2693
|
+
|
|
2694
|
+
expect(processCalls.length).toBeGreaterThan(0);
|
|
2695
|
+
expect(processCalls[0].options?.isInteractive).toBe(false);
|
|
2696
|
+
expect(deliverPromptSpy).not.toHaveBeenCalled();
|
|
2697
|
+
|
|
2698
|
+
deliverPromptSpy.mockRestore();
|
|
2699
|
+
});
|
|
2700
|
+
});
|
|
2701
|
+
|
|
2702
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2703
|
+
// NL approval routing via destination-scoped canonical requests
|
|
2704
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2705
|
+
|
|
2706
|
+
describe('NL approval routing via destination-scoped canonical requests', () => {
|
|
2707
|
+
beforeEach(() => {
|
|
2708
|
+
resetTables();
|
|
2709
|
+
noopProcessMessage.mockClear();
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
test('guardian plain-text "yes" resolves a pending_question with no guardianExternalUserId via delivery-scoped hint', async () => {
|
|
2713
|
+
// Simulate a voice-originated pending_question without guardianExternalUserId
|
|
2714
|
+
const guardianChatId = 'guardian-chat-nl-1';
|
|
2715
|
+
const guardianUserId = 'guardian-user-nl-1';
|
|
2716
|
+
|
|
2717
|
+
// Ensure the conversation exists so the resolver finds it
|
|
2718
|
+
ensureConversation('conv-voice-nl-1');
|
|
2719
|
+
|
|
2720
|
+
// Create guardian binding for Telegram
|
|
2721
|
+
createBinding({
|
|
2722
|
+
assistantId: 'self',
|
|
2723
|
+
channel: 'telegram',
|
|
2724
|
+
guardianExternalUserId: guardianUserId,
|
|
2725
|
+
guardianDeliveryChatId: guardianChatId,
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2728
|
+
// Create canonical tool_approval request WITHOUT guardianExternalUserId
|
|
2729
|
+
// but WITH a conversationId (required by the tool_approval resolver)
|
|
2730
|
+
const canonicalReq = createCanonicalGuardianRequest({
|
|
2731
|
+
kind: 'tool_approval',
|
|
2732
|
+
sourceType: 'voice',
|
|
2733
|
+
sourceChannel: 'twilio',
|
|
2734
|
+
conversationId: 'conv-voice-nl-1',
|
|
2735
|
+
toolName: 'shell',
|
|
2736
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
2737
|
+
// guardianExternalUserId intentionally omitted
|
|
2738
|
+
});
|
|
2739
|
+
|
|
2740
|
+
// Register pending interaction so resolver can find it
|
|
2741
|
+
registerPendingInteraction(canonicalReq.id, 'conv-voice-nl-1', 'shell');
|
|
2742
|
+
|
|
2743
|
+
// Create canonical delivery row targeting guardian chat
|
|
2744
|
+
createCanonicalGuardianDelivery({
|
|
2745
|
+
requestId: canonicalReq.id,
|
|
2746
|
+
destinationChannel: 'telegram',
|
|
2747
|
+
destinationChatId: guardianChatId,
|
|
2748
|
+
});
|
|
2749
|
+
|
|
2750
|
+
// Send inbound guardian text reply "yes" from that chat
|
|
2751
|
+
const req = makeInboundRequest({
|
|
2752
|
+
sourceChannel: 'telegram',
|
|
2753
|
+
externalChatId: guardianChatId,
|
|
2754
|
+
senderExternalUserId: guardianUserId,
|
|
2755
|
+
content: 'yes',
|
|
2756
|
+
externalMessageId: `msg-nl-approve-${Date.now()}`,
|
|
2757
|
+
});
|
|
2758
|
+
const res = await handleChannelInbound(req, noopProcessMessage as any, TEST_BEARER_TOKEN);
|
|
2759
|
+
const body = await res.json() as Record<string, unknown>;
|
|
2760
|
+
|
|
2761
|
+
expect(body.accepted).toBe(true);
|
|
2762
|
+
expect(body.canonicalRouter).toBe('canonical_decision_applied');
|
|
2763
|
+
|
|
2764
|
+
// Verify the request was resolved
|
|
2765
|
+
const resolved = getCanonicalGuardianRequest(canonicalReq.id);
|
|
2766
|
+
expect(resolved).not.toBeNull();
|
|
2767
|
+
expect(resolved!.status).toBe('approved');
|
|
2768
|
+
});
|
|
2769
|
+
|
|
2770
|
+
test('inbound from different chat ID does not auto-match delivery-scoped canonical request', async () => {
|
|
2771
|
+
const guardianChatId = 'guardian-chat-nl-2';
|
|
2772
|
+
const guardianUserId = 'guardian-user-nl-2';
|
|
2773
|
+
const differentChatId = 'different-chat-999';
|
|
2774
|
+
|
|
2775
|
+
// Create guardian binding for the guardian user on the different chat
|
|
2776
|
+
createBinding({
|
|
2777
|
+
assistantId: 'self',
|
|
2778
|
+
channel: 'telegram',
|
|
2779
|
+
guardianExternalUserId: guardianUserId,
|
|
2780
|
+
guardianDeliveryChatId: differentChatId,
|
|
2781
|
+
});
|
|
2782
|
+
|
|
2783
|
+
// Create canonical pending_question WITHOUT guardianExternalUserId
|
|
2784
|
+
const canonicalReq = createCanonicalGuardianRequest({
|
|
2785
|
+
kind: 'tool_approval',
|
|
2786
|
+
sourceType: 'voice',
|
|
2787
|
+
sourceChannel: 'twilio',
|
|
2788
|
+
toolName: 'shell',
|
|
2789
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
// Delivery targets the original guardian chat, NOT the different chat
|
|
2793
|
+
createCanonicalGuardianDelivery({
|
|
2794
|
+
requestId: canonicalReq.id,
|
|
2795
|
+
destinationChannel: 'telegram',
|
|
2796
|
+
destinationChatId: guardianChatId,
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
// Send from differentChatId — delivery-scoped lookup should not match
|
|
2800
|
+
const req = makeInboundRequest({
|
|
2801
|
+
sourceChannel: 'telegram',
|
|
2802
|
+
externalChatId: differentChatId,
|
|
2803
|
+
senderExternalUserId: guardianUserId,
|
|
2804
|
+
content: 'approve',
|
|
2805
|
+
externalMessageId: `msg-nl-mismatch-${Date.now()}`,
|
|
2806
|
+
});
|
|
2807
|
+
const res = await handleChannelInbound(req, noopProcessMessage as any, TEST_BEARER_TOKEN);
|
|
2808
|
+
const body = await res.json() as Record<string, unknown>;
|
|
2809
|
+
|
|
2810
|
+
expect(body.accepted).toBe(true);
|
|
2811
|
+
// Should NOT have been consumed by canonical router since there are no
|
|
2812
|
+
// delivery-scoped pending requests for this chat, and identity-based
|
|
2813
|
+
// fallback finds no match either (no guardianExternalUserId on request)
|
|
2814
|
+
expect(body.canonicalRouter).toBeUndefined();
|
|
2815
|
+
|
|
2816
|
+
// Request should remain pending
|
|
2817
|
+
const unchanged = getCanonicalGuardianRequest(canonicalReq.id);
|
|
2818
|
+
expect(unchanged).not.toBeNull();
|
|
2819
|
+
expect(unchanged!.status).toBe('pending');
|
|
2820
|
+
});
|
|
2648
2821
|
});
|
|
@@ -7,6 +7,7 @@ const runDeterministicChecksMock = mock();
|
|
|
7
7
|
const createEventMock = mock();
|
|
8
8
|
const updateEventDedupeKeyMock = mock();
|
|
9
9
|
const dispatchDecisionMock = mock();
|
|
10
|
+
const activeBindingChannels = new Set<string>(['telegram']);
|
|
10
11
|
|
|
11
12
|
mock.module('../util/logger.js', () => ({
|
|
12
13
|
getLogger: () =>
|
|
@@ -21,7 +22,7 @@ mock.module('../channels/config.js', () => ({
|
|
|
21
22
|
|
|
22
23
|
mock.module('../memory/channel-guardian-store.js', () => ({
|
|
23
24
|
getActiveBinding: (_assistantId: string, channel: string) =>
|
|
24
|
-
channel
|
|
25
|
+
activeBindingChannels.has(channel)
|
|
25
26
|
? {
|
|
26
27
|
guardianDeliveryChatId: 'guardian-chat-123',
|
|
27
28
|
guardianExternalUserId: 'guardian-user-123',
|
|
@@ -83,6 +84,8 @@ describe('emitNotificationSignal routing intent re-persistence', () => {
|
|
|
83
84
|
createEventMock.mockReset();
|
|
84
85
|
updateEventDedupeKeyMock.mockReset();
|
|
85
86
|
dispatchDecisionMock.mockReset();
|
|
87
|
+
activeBindingChannels.clear();
|
|
88
|
+
activeBindingChannels.add('telegram');
|
|
86
89
|
|
|
87
90
|
createEventMock.mockReturnValue({ id: 'evt-1' });
|
|
88
91
|
runDeterministicChecksMock.mockResolvedValue({ passed: true });
|
|
@@ -176,4 +179,43 @@ describe('emitNotificationSignal routing intent re-persistence', () => {
|
|
|
176
179
|
|
|
177
180
|
expect(updateDecisionMock).not.toHaveBeenCalled();
|
|
178
181
|
});
|
|
182
|
+
|
|
183
|
+
test('excludes unverified binding channels from connected channel candidates', async () => {
|
|
184
|
+
activeBindingChannels.clear();
|
|
185
|
+
|
|
186
|
+
const decision = {
|
|
187
|
+
shouldNotify: true,
|
|
188
|
+
selectedChannels: ['vellum'],
|
|
189
|
+
reasoningSummary: 'Local only',
|
|
190
|
+
renderedCopy: {
|
|
191
|
+
vellum: { title: 'Reminder', body: 'Check this' },
|
|
192
|
+
},
|
|
193
|
+
dedupeKey: 'dedupe-rem-3',
|
|
194
|
+
confidence: 0.8,
|
|
195
|
+
fallbackUsed: false,
|
|
196
|
+
persistedDecisionId: 'dec-3',
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
evaluateSignalMock.mockResolvedValue(decision);
|
|
200
|
+
enforceRoutingIntentMock.mockImplementation((inputDecision: unknown) => inputDecision);
|
|
201
|
+
|
|
202
|
+
await emitNotificationSignal({
|
|
203
|
+
sourceEventName: 'reminder.fired',
|
|
204
|
+
sourceChannel: 'scheduler',
|
|
205
|
+
sourceSessionId: 'rem-3',
|
|
206
|
+
attentionHints: {
|
|
207
|
+
requiresAction: false,
|
|
208
|
+
urgency: 'medium',
|
|
209
|
+
isAsyncBackground: false,
|
|
210
|
+
visibleInSourceNow: false,
|
|
211
|
+
},
|
|
212
|
+
contextPayload: { reminderId: 'rem-3' },
|
|
213
|
+
routingIntent: 'single_channel',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(evaluateSignalMock).toHaveBeenCalled();
|
|
217
|
+
const callArgs = evaluateSignalMock.mock.calls[0];
|
|
218
|
+
expect(callArgs).toBeDefined();
|
|
219
|
+
expect(callArgs?.[1]).toEqual(['vellum']);
|
|
220
|
+
});
|
|
179
221
|
});
|