@vellumai/assistant 0.4.5 → 0.4.7
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 +27 -10
- package/README.md +6 -6
- package/bun.lock +57 -2
- package/docs/architecture/memory.md +4 -4
- package/docs/trusted-contact-access.md +8 -0
- package/package.json +3 -2
- package/src/__tests__/actor-token-service.test.ts +9 -6
- package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/call-controller.test.ts +115 -0
- package/src/__tests__/call-domain.test.ts +148 -10
- package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
- package/src/__tests__/call-pointer-messages.test.ts +105 -43
- package/src/__tests__/canonical-guardian-store.test.ts +44 -10
- package/src/__tests__/channel-approval-routes.test.ts +67 -65
- package/src/__tests__/channel-delivery-store.test.ts +2 -2
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
- package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
- package/src/__tests__/gateway-client-managed-outbound.test.ts +147 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
- package/src/__tests__/guardian-dispatch.test.ts +39 -1
- package/src/__tests__/guardian-grant-minting.test.ts +24 -24
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
- package/src/__tests__/guardian-routing-state.test.ts +10 -32
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
- package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
- package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
- package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +57 -47
- package/src/__tests__/notification-decision-fallback.test.ts +232 -0
- package/src/__tests__/notification-decision-strategy.test.ts +304 -8
- package/src/__tests__/notification-guardian-path.test.ts +38 -1
- package/src/__tests__/relay-server.test.ts +136 -5
- package/src/__tests__/send-endpoint-busy.test.ts +35 -1
- package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
- package/src/__tests__/system-prompt.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +1 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +10 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +14 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +24 -24
- package/src/__tests__/trusted-contact-multichannel.test.ts +5 -5
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/approvals/guardian-decision-primitive.ts +29 -25
- package/src/approvals/guardian-request-resolvers.ts +9 -5
- package/src/calls/call-controller.ts +15 -0
- package/src/calls/call-pointer-message-composer.ts +27 -85
- package/src/calls/call-pointer-messages.ts +54 -21
- package/src/calls/guardian-dispatch.ts +30 -0
- package/src/calls/relay-server.ts +58 -24
- package/src/calls/types.ts +1 -0
- package/src/config/system-prompt.ts +10 -3
- package/src/config/templates/BOOTSTRAP.md +6 -5
- package/src/config/templates/USER.md +1 -0
- package/src/config/user-reference.ts +44 -0
- package/src/daemon/handlers/guardian-actions.ts +5 -2
- package/src/daemon/handlers/sessions.ts +8 -3
- package/src/daemon/lifecycle.ts +109 -3
- package/src/daemon/providers-setup.ts +0 -8
- package/src/daemon/server.ts +32 -24
- package/src/daemon/session-agent-loop.ts +4 -3
- package/src/daemon/session-lifecycle.ts +1 -9
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-runtime-assembly.ts +2 -0
- package/src/daemon/session-slash.ts +35 -2
- package/src/daemon/session-tool-setup.ts +10 -0
- package/src/daemon/session.ts +1 -0
- package/src/memory/canonical-guardian-store.ts +40 -0
- package/src/memory/conversation-crud.ts +26 -0
- package/src/memory/conversation-store.ts +1 -0
- package/src/memory/db-init.ts +12 -0
- package/src/memory/guardian-bindings.ts +4 -0
- package/src/memory/job-handlers/backfill.ts +2 -9
- package/src/memory/migrations/039-actor-refresh-token-records.ts +51 -0
- package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema.ts +22 -0
- package/src/notifications/README.md +8 -1
- package/src/notifications/copy-composer.ts +160 -30
- package/src/notifications/decision-engine.ts +98 -1
- package/src/runtime/access-request-helper.ts +43 -28
- package/src/runtime/actor-refresh-token-service.ts +309 -0
- package/src/runtime/actor-refresh-token-store.ts +157 -0
- package/src/runtime/actor-token-service.ts +3 -3
- package/src/runtime/actor-trust-resolver.ts +19 -14
- package/src/runtime/channel-guardian-service.ts +6 -0
- package/src/runtime/gateway-client.ts +239 -0
- package/src/runtime/guardian-context-resolver.ts +6 -2
- package/src/runtime/guardian-reply-router.ts +33 -16
- package/src/runtime/guardian-vellum-migration.ts +29 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/http-types.ts +0 -13
- package/src/runtime/local-actor-identity.ts +19 -13
- package/src/runtime/middleware/actor-token.ts +2 -2
- package/src/runtime/routes/channel-delivery-routes.ts +5 -5
- package/src/runtime/routes/conversation-routes.ts +45 -35
- package/src/runtime/routes/guardian-action-routes.ts +7 -1
- package/src/runtime/routes/guardian-approval-interception.ts +52 -52
- package/src/runtime/routes/guardian-bootstrap-routes.ts +11 -24
- package/src/runtime/routes/guardian-refresh-routes.ts +53 -0
- package/src/runtime/routes/inbound-conversation.ts +7 -7
- package/src/runtime/routes/inbound-message-handler.ts +105 -94
- package/src/runtime/routes/pairing-routes.ts +60 -50
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/types/qrcode.d.ts +10 -0
- package/src/util/logger.ts +10 -0
- package/src/daemon/call-pointer-generators.ts +0 -59
|
@@ -508,7 +508,7 @@ describe('channel-delivery-store', () => {
|
|
|
508
508
|
headers: { 'Content-Type': 'application/json' },
|
|
509
509
|
body: JSON.stringify({
|
|
510
510
|
sourceChannel: 'telegram',
|
|
511
|
-
|
|
511
|
+
conversationExternalId: 'chat-del',
|
|
512
512
|
// Note: no assistantId in the body — it comes from the route param
|
|
513
513
|
}),
|
|
514
514
|
});
|
|
@@ -558,7 +558,7 @@ describe('channel-delivery-store', () => {
|
|
|
558
558
|
headers: { 'Content-Type': 'application/json' },
|
|
559
559
|
body: JSON.stringify({
|
|
560
560
|
sourceChannel: 'telegram',
|
|
561
|
-
|
|
561
|
+
conversationExternalId: 'chat-def',
|
|
562
562
|
}),
|
|
563
563
|
});
|
|
564
564
|
|
|
@@ -118,6 +118,7 @@ function makeCanonicalRequest(overrides: Record<string, unknown> = {}) {
|
|
|
118
118
|
conversationId: 'conv-1',
|
|
119
119
|
requesterExternalUserId: 'requester-1',
|
|
120
120
|
guardianExternalUserId: 'guardian-1',
|
|
121
|
+
guardianPrincipalId: 'test-principal-id',
|
|
121
122
|
toolName: 'bash',
|
|
122
123
|
status: 'pending',
|
|
123
124
|
requestCode: generateCanonicalRequestCode(),
|
|
@@ -119,8 +119,8 @@ function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
|
119
119
|
const body = {
|
|
120
120
|
sourceChannel: 'telegram',
|
|
121
121
|
interface: 'telegram',
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
conversationExternalId: 'chat-123',
|
|
123
|
+
actorExternalId: 'telegram-user-default',
|
|
124
124
|
externalMessageId: `msg-${Date.now()}-${Math.random()}`,
|
|
125
125
|
content: 'hello',
|
|
126
126
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
@@ -257,11 +257,11 @@ describe('Verification control messages are deterministic (guard)', () => {
|
|
|
257
257
|
body: JSON.stringify({
|
|
258
258
|
sourceChannel: 'telegram',
|
|
259
259
|
interface: 'telegram',
|
|
260
|
-
|
|
260
|
+
conversationExternalId: 'chat-123',
|
|
261
261
|
externalMessageId: `msg-guard-${Date.now()}`,
|
|
262
262
|
content: secret,
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
actorExternalId: 'user-123',
|
|
264
|
+
actorDisplayName: 'Test User',
|
|
265
265
|
replyCallbackUrl: 'http://localhost/callback',
|
|
266
266
|
}),
|
|
267
267
|
});
|
|
@@ -330,11 +330,11 @@ describe('Verification control messages are deterministic (guard)', () => {
|
|
|
330
330
|
body: JSON.stringify({
|
|
331
331
|
sourceChannel: 'telegram',
|
|
332
332
|
interface: 'telegram',
|
|
333
|
-
|
|
333
|
+
conversationExternalId: 'chat-bootstrap-123',
|
|
334
334
|
externalMessageId: `msg-bootstrap-${Date.now()}`,
|
|
335
335
|
content: `/start gv_${bootstrapToken}`,
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
actorExternalId: 'user-bootstrap-123',
|
|
337
|
+
actorDisplayName: 'Bootstrap User',
|
|
338
338
|
replyCallbackUrl: 'http://localhost/callback',
|
|
339
339
|
sourceMetadata: {
|
|
340
340
|
commandIntent: { type: 'start', payload: `gv_${bootstrapToken}` },
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { deliverChannelReply } from '../runtime/gateway-client.js';
|
|
4
|
+
|
|
5
|
+
type FetchCall = {
|
|
6
|
+
url: string;
|
|
7
|
+
init: RequestInit;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe('gateway-client managed outbound lane', () => {
|
|
11
|
+
const originalFetch = globalThis.fetch;
|
|
12
|
+
const calls: FetchCall[] = [];
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
calls.length = 0;
|
|
16
|
+
globalThis.fetch = mock(async (input: string | URL | Request, init?: RequestInit) => {
|
|
17
|
+
const url = typeof input === 'string'
|
|
18
|
+
? input
|
|
19
|
+
: input instanceof URL
|
|
20
|
+
? input.toString()
|
|
21
|
+
: input.url;
|
|
22
|
+
calls.push({ url, init: init ?? {} });
|
|
23
|
+
return new Response(JSON.stringify({ status: 'accepted' }), { status: 202 });
|
|
24
|
+
}) as unknown as typeof globalThis.fetch;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
globalThis.fetch = originalFetch;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('translates managed callback URL into managed outbound-send request', async () => {
|
|
32
|
+
await deliverChannelReply(
|
|
33
|
+
'https://platform.test/v1/internal/managed-gateway/outbound-send/?route_id=route-123&assistant_id=assistant-123&source_channel=sms&source_update_id=SM-inbound-123&callback_token=runtime-token',
|
|
34
|
+
{
|
|
35
|
+
chatId: '+15550001111',
|
|
36
|
+
text: 'hello from runtime',
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(calls).toHaveLength(1);
|
|
41
|
+
const call = calls[0];
|
|
42
|
+
expect(call.url).toBe('https://platform.test/v1/internal/managed-gateway/outbound-send/');
|
|
43
|
+
|
|
44
|
+
const headers = call.init.headers as Record<string, string>;
|
|
45
|
+
expect(headers['Content-Type']).toBe('application/json');
|
|
46
|
+
expect(headers['X-Managed-Gateway-Callback-Token']).toBe('runtime-token');
|
|
47
|
+
expect(headers['X-Idempotency-Key']).toStartWith('mgw-send-');
|
|
48
|
+
expect(headers.Authorization).toBeUndefined();
|
|
49
|
+
|
|
50
|
+
const body = JSON.parse(String(call.init.body)) as {
|
|
51
|
+
route_id: string;
|
|
52
|
+
assistant_id: string;
|
|
53
|
+
normalized_send: {
|
|
54
|
+
sourceChannel: string;
|
|
55
|
+
message: {
|
|
56
|
+
to: string;
|
|
57
|
+
content: string;
|
|
58
|
+
externalMessageId: string;
|
|
59
|
+
};
|
|
60
|
+
source: {
|
|
61
|
+
requestId: string;
|
|
62
|
+
};
|
|
63
|
+
raw: {
|
|
64
|
+
sourceUpdateId: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
expect(body.route_id).toBe('route-123');
|
|
69
|
+
expect(body.assistant_id).toBe('assistant-123');
|
|
70
|
+
expect(body.normalized_send.sourceChannel).toBe('sms');
|
|
71
|
+
expect(body.normalized_send.message.to).toBe('+15550001111');
|
|
72
|
+
expect(body.normalized_send.message.content).toBe('hello from runtime');
|
|
73
|
+
expect(body.normalized_send.message.externalMessageId).toStartWith('mgw-send-');
|
|
74
|
+
expect(body.normalized_send.source.requestId).toBe(
|
|
75
|
+
body.normalized_send.message.externalMessageId,
|
|
76
|
+
);
|
|
77
|
+
expect(body.normalized_send.raw.sourceUpdateId).toBe('SM-inbound-123');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('retries managed outbound send on retriable upstream responses with stable idempotency key', async () => {
|
|
81
|
+
calls.length = 0;
|
|
82
|
+
let attempt = 0;
|
|
83
|
+
globalThis.fetch = mock(async (input: string | URL | Request, init?: RequestInit) => {
|
|
84
|
+
const url = typeof input === 'string'
|
|
85
|
+
? input
|
|
86
|
+
: input instanceof URL
|
|
87
|
+
? input.toString()
|
|
88
|
+
: input.url;
|
|
89
|
+
calls.push({ url, init: init ?? {} });
|
|
90
|
+
attempt += 1;
|
|
91
|
+
if (attempt === 1) {
|
|
92
|
+
return new Response('temporary upstream error', { status: 502 });
|
|
93
|
+
}
|
|
94
|
+
return new Response(JSON.stringify({ status: 'accepted' }), { status: 202 });
|
|
95
|
+
}) as unknown as typeof globalThis.fetch;
|
|
96
|
+
|
|
97
|
+
await deliverChannelReply(
|
|
98
|
+
'https://platform.test/v1/internal/managed-gateway/outbound-send/?route_id=route-retry&assistant_id=assistant-retry&source_channel=sms&source_update_id=SM-retry',
|
|
99
|
+
{
|
|
100
|
+
chatId: '+15550002222',
|
|
101
|
+
text: 'retry this outbound send',
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(calls).toHaveLength(2);
|
|
106
|
+
|
|
107
|
+
const firstHeaders = calls[0].init.headers as Record<string, string>;
|
|
108
|
+
const secondHeaders = calls[1].init.headers as Record<string, string>;
|
|
109
|
+
expect(firstHeaders['X-Idempotency-Key']).toBeDefined();
|
|
110
|
+
expect(secondHeaders['X-Idempotency-Key']).toBe(firstHeaders['X-Idempotency-Key']);
|
|
111
|
+
|
|
112
|
+
const firstBody = JSON.parse(String(calls[0].init.body)) as {
|
|
113
|
+
normalized_send: { source: { requestId: string } };
|
|
114
|
+
};
|
|
115
|
+
const secondBody = JSON.parse(String(calls[1].init.body)) as {
|
|
116
|
+
normalized_send: { source: { requestId: string } };
|
|
117
|
+
};
|
|
118
|
+
expect(secondBody.normalized_send.source.requestId).toBe(
|
|
119
|
+
firstBody.normalized_send.source.requestId,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('falls back to standard callback delivery for non-managed callback URL', async () => {
|
|
124
|
+
await deliverChannelReply(
|
|
125
|
+
'https://gateway.test/deliver/sms',
|
|
126
|
+
{
|
|
127
|
+
chatId: '+15550001111',
|
|
128
|
+
text: 'standard gateway callback',
|
|
129
|
+
},
|
|
130
|
+
'runtime-bearer',
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(calls).toHaveLength(1);
|
|
134
|
+
const call = calls[0];
|
|
135
|
+
expect(call.url).toBe('https://gateway.test/deliver/sms');
|
|
136
|
+
|
|
137
|
+
const headers = call.init.headers as Record<string, string>;
|
|
138
|
+
expect(headers['Content-Type']).toBe('application/json');
|
|
139
|
+
expect(headers.Authorization).toBe('Bearer runtime-bearer');
|
|
140
|
+
|
|
141
|
+
const body = JSON.parse(String(call.init.body)) as { chatId: string; text: string };
|
|
142
|
+
expect(body).toEqual({
|
|
143
|
+
chatId: '+15550001111',
|
|
144
|
+
text: 'standard gateway callback',
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -94,6 +94,7 @@ function createTestCanonicalRequest(overrides: {
|
|
|
94
94
|
kind?: string;
|
|
95
95
|
toolName?: string;
|
|
96
96
|
guardianExternalUserId?: string;
|
|
97
|
+
guardianPrincipalId?: string;
|
|
97
98
|
questionText?: string;
|
|
98
99
|
expiresAt?: string;
|
|
99
100
|
}) {
|
|
@@ -105,6 +106,7 @@ function createTestCanonicalRequest(overrides: {
|
|
|
105
106
|
sourceChannel: 'vellum',
|
|
106
107
|
conversationId: overrides.conversationId,
|
|
107
108
|
guardianExternalUserId: overrides.guardianExternalUserId,
|
|
109
|
+
guardianPrincipalId: overrides.guardianPrincipalId ?? 'test-principal',
|
|
108
110
|
toolName: overrides.toolName ?? 'bash',
|
|
109
111
|
questionText: overrides.questionText,
|
|
110
112
|
requestCode: generateCanonicalRequestCode(),
|
|
@@ -304,7 +306,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
304
306
|
expect(body.requestId).toBe('req-stale-1');
|
|
305
307
|
});
|
|
306
308
|
|
|
307
|
-
test('passes actorContext with
|
|
309
|
+
test('passes actorContext with vellum channel and guardianPrincipalId', async () => {
|
|
308
310
|
createTestCanonicalRequest({ conversationId: 'conv-actor', requestId: 'req-actor-1' });
|
|
309
311
|
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-actor-1', grantMinted: false });
|
|
310
312
|
|
|
@@ -315,9 +317,8 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
315
317
|
await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
316
318
|
const call = mockApplyCanonicalGuardianDecision.mock.calls[0]![0] as Record<string, unknown>;
|
|
317
319
|
const actorContext = call.actorContext as Record<string, unknown>;
|
|
318
|
-
expect(actorContext.externalUserId).toBeUndefined();
|
|
319
320
|
expect(actorContext.channel).toBe('vellum');
|
|
320
|
-
expect(actorContext.
|
|
321
|
+
expect(actorContext.guardianPrincipalId).toBeDefined();
|
|
321
322
|
});
|
|
322
323
|
});
|
|
323
324
|
|
|
@@ -373,6 +374,7 @@ describe('listGuardianDecisionPrompts', () => {
|
|
|
373
374
|
sourceType: 'desktop',
|
|
374
375
|
sourceChannel: 'vellum',
|
|
375
376
|
conversationId: 'conv-expired',
|
|
377
|
+
guardianPrincipalId: 'test-principal',
|
|
376
378
|
toolName: 'bash',
|
|
377
379
|
requestCode: generateCanonicalRequestCode(),
|
|
378
380
|
status: 'pending',
|
|
@@ -597,7 +599,7 @@ describe('IPC guardian_action_decision', () => {
|
|
|
597
599
|
expect(sent[0].reason).toBe('already_resolved');
|
|
598
600
|
});
|
|
599
601
|
|
|
600
|
-
test('passes actorContext with
|
|
602
|
+
test('passes actorContext with vellum channel and guardianPrincipalId', async () => {
|
|
601
603
|
createTestCanonicalRequest({ conversationId: 'conv-ipc-actor', requestId: 'req-ipc-actor' });
|
|
602
604
|
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-ipc-actor', grantMinted: false });
|
|
603
605
|
|
|
@@ -609,9 +611,8 @@ describe('IPC guardian_action_decision', () => {
|
|
|
609
611
|
);
|
|
610
612
|
const call = mockApplyCanonicalGuardianDecision.mock.calls[0]![0] as Record<string, unknown>;
|
|
611
613
|
const actorContext = call.actorContext as Record<string, unknown>;
|
|
612
|
-
expect(actorContext.externalUserId).toBeUndefined();
|
|
613
614
|
expect(actorContext.channel).toBe('vellum');
|
|
614
|
-
expect(actorContext.
|
|
615
|
+
expect(actorContext.guardianPrincipalId).toBeDefined();
|
|
615
616
|
});
|
|
616
617
|
});
|
|
617
618
|
|
|
@@ -64,11 +64,14 @@ afterAll(() => {
|
|
|
64
64
|
// Helpers
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
|
|
67
|
+
/** Consistent test principal used across all test actors and requests. */
|
|
68
|
+
const TEST_PRINCIPAL_ID = 'test-principal-id';
|
|
69
|
+
|
|
67
70
|
function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
68
71
|
return {
|
|
69
72
|
externalUserId: 'guardian-1',
|
|
70
73
|
channel: 'telegram',
|
|
71
|
-
|
|
74
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
72
75
|
...overrides,
|
|
73
76
|
};
|
|
74
77
|
}
|
|
@@ -77,7 +80,7 @@ function trustedActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
|
77
80
|
return {
|
|
78
81
|
externalUserId: undefined,
|
|
79
82
|
channel: 'desktop',
|
|
80
|
-
|
|
83
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
81
84
|
...overrides,
|
|
82
85
|
};
|
|
83
86
|
}
|
|
@@ -120,6 +123,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
120
123
|
sourceChannel: 'telegram',
|
|
121
124
|
conversationId: 'conv-1',
|
|
122
125
|
guardianExternalUserId: 'guardian-1',
|
|
126
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
123
127
|
toolName: 'shell',
|
|
124
128
|
inputDigest: 'sha256:abc',
|
|
125
129
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -153,6 +157,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
153
157
|
sourceChannel: 'telegram',
|
|
154
158
|
conversationId: 'conv-1',
|
|
155
159
|
guardianExternalUserId: 'guardian-1',
|
|
160
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
156
161
|
toolName: 'shell',
|
|
157
162
|
inputDigest: 'sha256:abc',
|
|
158
163
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -178,6 +183,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
178
183
|
sourceType: 'voice',
|
|
179
184
|
sourceChannel: 'twilio',
|
|
180
185
|
guardianExternalUserId: 'guardian-1',
|
|
186
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
181
187
|
callSessionId: 'call-1',
|
|
182
188
|
pendingQuestionId: 'pq-1',
|
|
183
189
|
questionText: 'What is the gate code?',
|
|
@@ -199,21 +205,22 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
199
205
|
expect(resolved!.answerText).toBe('1234');
|
|
200
206
|
});
|
|
201
207
|
|
|
202
|
-
// ──
|
|
208
|
+
// ── Principal mismatch ──────────────────────────────────────────────
|
|
203
209
|
|
|
204
|
-
test('rejects decision when actor does not match
|
|
210
|
+
test('rejects decision when actor principal does not match request principal', async () => {
|
|
205
211
|
const req = createCanonicalGuardianRequest({
|
|
206
212
|
kind: 'tool_approval',
|
|
207
213
|
sourceType: 'channel',
|
|
208
214
|
conversationId: 'conv-1',
|
|
209
215
|
guardianExternalUserId: 'guardian-1',
|
|
216
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
210
217
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
211
218
|
});
|
|
212
219
|
|
|
213
220
|
const result = await applyCanonicalGuardianDecision({
|
|
214
221
|
requestId: req.id,
|
|
215
222
|
action: 'approve_once',
|
|
216
|
-
actorContext: guardianActor({
|
|
223
|
+
actorContext: guardianActor({ guardianPrincipalId: 'wrong-principal' }),
|
|
217
224
|
});
|
|
218
225
|
|
|
219
226
|
expect(result.applied).toBe(false);
|
|
@@ -225,12 +232,13 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
225
232
|
expect(unchanged!.status).toBe('pending');
|
|
226
233
|
});
|
|
227
234
|
|
|
228
|
-
test('
|
|
235
|
+
test('matching principal authorizes decision (cross-channel same principal)', async () => {
|
|
229
236
|
const req = createCanonicalGuardianRequest({
|
|
230
237
|
kind: 'tool_approval',
|
|
231
238
|
sourceType: 'desktop',
|
|
232
239
|
conversationId: 'conv-1',
|
|
233
240
|
guardianExternalUserId: 'guardian-1',
|
|
241
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
234
242
|
toolName: 'shell',
|
|
235
243
|
inputDigest: 'sha256:abc',
|
|
236
244
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -243,24 +251,48 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
243
251
|
});
|
|
244
252
|
|
|
245
253
|
expect(result.applied).toBe(true);
|
|
246
|
-
// No grant minted because trusted actor has no externalUserId
|
|
247
254
|
if (!result.applied) return;
|
|
255
|
+
// No grant minted because trusted actor has no externalUserId
|
|
248
256
|
expect(result.grantMinted).toBe(false);
|
|
249
257
|
});
|
|
250
258
|
|
|
251
|
-
test('rejects
|
|
259
|
+
test('rejects decision when request has no guardianPrincipalId', async () => {
|
|
260
|
+
// unknown_kind is not in DECISIONABLE_KINDS so it can be created without
|
|
261
|
+
// guardianPrincipalId, but the decision primitive still rejects because
|
|
262
|
+
// the request is missing its principal binding.
|
|
263
|
+
const req = createCanonicalGuardianRequest({
|
|
264
|
+
kind: 'unknown_kind',
|
|
265
|
+
sourceType: 'channel',
|
|
266
|
+
conversationId: 'conv-1',
|
|
267
|
+
guardianExternalUserId: 'guardian-1',
|
|
268
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const result = await applyCanonicalGuardianDecision({
|
|
272
|
+
requestId: req.id,
|
|
273
|
+
action: 'approve_once',
|
|
274
|
+
actorContext: guardianActor({ guardianPrincipalId: 'some-principal' }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(result.applied).toBe(false);
|
|
278
|
+
if (result.applied) return;
|
|
279
|
+
expect(result.reason).toBe('identity_mismatch');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('rejects decision when actor has no guardianPrincipalId', async () => {
|
|
252
283
|
const req = createCanonicalGuardianRequest({
|
|
253
284
|
kind: 'tool_approval',
|
|
254
285
|
sourceType: 'channel',
|
|
255
286
|
conversationId: 'conv-1',
|
|
256
|
-
|
|
287
|
+
guardianExternalUserId: 'guardian-1',
|
|
288
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
257
289
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
258
290
|
});
|
|
259
291
|
|
|
260
292
|
const result = await applyCanonicalGuardianDecision({
|
|
261
293
|
requestId: req.id,
|
|
262
294
|
action: 'approve_once',
|
|
263
|
-
actorContext: guardianActor({
|
|
295
|
+
actorContext: guardianActor({ guardianPrincipalId: undefined }),
|
|
264
296
|
});
|
|
265
297
|
|
|
266
298
|
expect(result.applied).toBe(false);
|
|
@@ -276,6 +308,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
276
308
|
sourceType: 'channel',
|
|
277
309
|
conversationId: 'conv-1',
|
|
278
310
|
guardianExternalUserId: 'guardian-1',
|
|
311
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
279
312
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
280
313
|
});
|
|
281
314
|
|
|
@@ -324,6 +357,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
324
357
|
sourceType: 'channel',
|
|
325
358
|
conversationId: 'conv-1',
|
|
326
359
|
guardianExternalUserId: 'guardian-1',
|
|
360
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
327
361
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
328
362
|
});
|
|
329
363
|
|
|
@@ -351,6 +385,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
351
385
|
sourceChannel: 'telegram',
|
|
352
386
|
conversationId: 'conv-1',
|
|
353
387
|
guardianExternalUserId: 'guardian-1',
|
|
388
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
354
389
|
toolName: 'shell',
|
|
355
390
|
inputDigest: 'sha256:abc',
|
|
356
391
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -385,6 +420,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
385
420
|
sourceType: 'channel',
|
|
386
421
|
conversationId: 'conv-1',
|
|
387
422
|
guardianExternalUserId: 'guardian-1',
|
|
423
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
388
424
|
expiresAt: new Date(Date.now() - 10_000).toISOString(), // already expired
|
|
389
425
|
});
|
|
390
426
|
|
|
@@ -405,6 +441,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
405
441
|
sourceType: 'channel',
|
|
406
442
|
conversationId: 'conv-1',
|
|
407
443
|
guardianExternalUserId: 'guardian-1',
|
|
444
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
408
445
|
// No expiresAt
|
|
409
446
|
});
|
|
410
447
|
|
|
@@ -426,6 +463,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
426
463
|
sourceChannel: 'telegram',
|
|
427
464
|
conversationId: 'conv-1',
|
|
428
465
|
guardianExternalUserId: 'guardian-1',
|
|
466
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
429
467
|
toolName: 'file_read',
|
|
430
468
|
inputDigest: 'sha256:def',
|
|
431
469
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -446,6 +484,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
446
484
|
sourceType: 'voice',
|
|
447
485
|
sourceChannel: 'twilio',
|
|
448
486
|
guardianExternalUserId: 'guardian-1',
|
|
487
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
449
488
|
callSessionId: 'call-99',
|
|
450
489
|
pendingQuestionId: 'pq-99',
|
|
451
490
|
questionText: 'What is the password?',
|
|
@@ -464,12 +503,13 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
464
503
|
expect(resolved!.answerText).toBe('secret123');
|
|
465
504
|
});
|
|
466
505
|
|
|
467
|
-
test('succeeds
|
|
506
|
+
test('succeeds for non-decisionable kind with matching principal', async () => {
|
|
468
507
|
const req = createCanonicalGuardianRequest({
|
|
469
508
|
kind: 'unknown_kind',
|
|
470
509
|
sourceType: 'channel',
|
|
471
510
|
conversationId: 'conv-1',
|
|
472
511
|
guardianExternalUserId: 'guardian-1',
|
|
512
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
473
513
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
474
514
|
});
|
|
475
515
|
|
|
@@ -485,7 +525,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
485
525
|
expect(resolved!.status).toBe('approved');
|
|
486
526
|
});
|
|
487
527
|
|
|
488
|
-
test('
|
|
528
|
+
test('desktop actor with matching principal mints scoped grant for approved canonical request', async () => {
|
|
489
529
|
const req = createCanonicalGuardianRequest({
|
|
490
530
|
kind: 'unknown_kind',
|
|
491
531
|
sourceType: 'voice',
|
|
@@ -494,6 +534,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
494
534
|
callSessionId: 'call-voice-1',
|
|
495
535
|
toolName: 'host_bash',
|
|
496
536
|
inputDigest: 'sha256:voice-digest-1',
|
|
537
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
497
538
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
498
539
|
});
|
|
499
540
|
|
|
@@ -530,6 +571,7 @@ describe('mintCanonicalRequestGrant', () => {
|
|
|
530
571
|
sourceType: 'channel',
|
|
531
572
|
sourceChannel: 'telegram',
|
|
532
573
|
conversationId: 'conv-1',
|
|
574
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
533
575
|
toolName: 'shell',
|
|
534
576
|
inputDigest: 'sha256:abc',
|
|
535
577
|
});
|
|
@@ -549,6 +591,7 @@ describe('mintCanonicalRequestGrant', () => {
|
|
|
549
591
|
sourceType: 'channel',
|
|
550
592
|
sourceChannel: 'telegram',
|
|
551
593
|
conversationId: 'conv-2',
|
|
594
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
552
595
|
toolName: 'shell',
|
|
553
596
|
inputDigest: 'sha256:xyz',
|
|
554
597
|
});
|
|
@@ -570,6 +613,7 @@ describe('mintCanonicalRequestGrant', () => {
|
|
|
570
613
|
const req = createCanonicalGuardianRequest({
|
|
571
614
|
kind: 'pending_question',
|
|
572
615
|
sourceType: 'voice',
|
|
616
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
573
617
|
// No toolName or inputDigest
|
|
574
618
|
});
|
|
575
619
|
|
|
@@ -586,6 +630,7 @@ describe('mintCanonicalRequestGrant', () => {
|
|
|
586
630
|
const req = createCanonicalGuardianRequest({
|
|
587
631
|
kind: 'tool_approval',
|
|
588
632
|
sourceType: 'channel',
|
|
633
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
589
634
|
toolName: 'shell',
|
|
590
635
|
// No inputDigest
|
|
591
636
|
});
|
|
@@ -31,25 +31,45 @@ mock.module('../util/logger.js', () => ({
|
|
|
31
31
|
|
|
32
32
|
let mockTelegramBinding: unknown = null;
|
|
33
33
|
let mockSmsBinding: unknown = null;
|
|
34
|
+
let mockVellumBinding: unknown = null;
|
|
34
35
|
|
|
35
36
|
mock.module('../memory/channel-guardian-store.js', () => ({
|
|
36
37
|
getActiveBinding: (_assistantId: string, channel: string) => {
|
|
37
38
|
if (channel === 'telegram') return mockTelegramBinding;
|
|
38
39
|
if (channel === 'sms') return mockSmsBinding;
|
|
40
|
+
if (channel === 'vellum') return mockVellumBinding;
|
|
39
41
|
return null;
|
|
40
42
|
},
|
|
43
|
+
createBinding: (params: Record<string, unknown>) => ({
|
|
44
|
+
id: `binding-${Date.now()}`,
|
|
45
|
+
...params,
|
|
46
|
+
status: 'active',
|
|
47
|
+
verifiedAt: Date.now(),
|
|
48
|
+
verifiedVia: 'test',
|
|
49
|
+
metadataJson: null,
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
updatedAt: Date.now(),
|
|
52
|
+
}),
|
|
53
|
+
listActiveBindingsByAssistant: () => mockVellumBinding ? [mockVellumBinding] : [],
|
|
41
54
|
}));
|
|
42
55
|
|
|
43
56
|
mock.module('../config/loader.js', () => ({
|
|
44
57
|
getConfig: () => ({
|
|
45
58
|
ui: {},
|
|
46
|
-
|
|
59
|
+
|
|
47
60
|
calls: {
|
|
48
61
|
userConsultTimeoutSeconds: 120,
|
|
49
62
|
},
|
|
50
63
|
}),
|
|
51
64
|
}));
|
|
52
65
|
|
|
66
|
+
// Mock guardian-vellum-migration to use a stable principal, avoiding UNIQUE
|
|
67
|
+
// constraint errors when ensureVellumGuardianBinding is called across tests.
|
|
68
|
+
// Returns a known principal so guardian-dispatch can attribute requests.
|
|
69
|
+
mock.module('../runtime/guardian-vellum-migration.js', () => ({
|
|
70
|
+
ensureVellumGuardianBinding: () => 'test-principal-id',
|
|
71
|
+
}));
|
|
72
|
+
|
|
53
73
|
const emitCalls: unknown[] = [];
|
|
54
74
|
let threadCreatedFromMock: ThreadCreatedInfo | null = null;
|
|
55
75
|
let mockEmitResult: {
|
|
@@ -109,12 +129,30 @@ function resetTables(): void {
|
|
|
109
129
|
db.run('DELETE FROM canonical_guardian_requests');
|
|
110
130
|
db.run('DELETE FROM guardian_action_deliveries');
|
|
111
131
|
db.run('DELETE FROM guardian_action_requests');
|
|
132
|
+
db.run('DELETE FROM channel_guardian_bindings');
|
|
112
133
|
db.run('DELETE FROM call_pending_questions');
|
|
113
134
|
db.run('DELETE FROM call_events');
|
|
114
135
|
db.run('DELETE FROM call_sessions');
|
|
115
136
|
db.run('DELETE FROM conversations');
|
|
137
|
+
|
|
116
138
|
mockTelegramBinding = null;
|
|
117
139
|
mockSmsBinding = null;
|
|
140
|
+
// Pre-seed vellum binding so the self-healing path in dispatchGuardianQuestion
|
|
141
|
+
// never triggers (avoids UNIQUE constraint violations on repeated dispatches).
|
|
142
|
+
mockVellumBinding = {
|
|
143
|
+
id: 'binding-vellum-test',
|
|
144
|
+
assistantId: 'self',
|
|
145
|
+
channel: 'vellum',
|
|
146
|
+
guardianExternalUserId: 'vellum-guardian',
|
|
147
|
+
guardianDeliveryChatId: 'local',
|
|
148
|
+
guardianPrincipalId: 'test-principal-id',
|
|
149
|
+
status: 'active',
|
|
150
|
+
verifiedAt: Date.now(),
|
|
151
|
+
verifiedVia: 'test',
|
|
152
|
+
metadataJson: null,
|
|
153
|
+
createdAt: Date.now(),
|
|
154
|
+
updatedAt: Date.now(),
|
|
155
|
+
};
|
|
118
156
|
emitCalls.length = 0;
|
|
119
157
|
threadCreatedFromMock = null;
|
|
120
158
|
mockEmitResult = {
|