@vellumai/assistant 0.4.4 → 0.4.6
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 +4 -4
- package/README.md +6 -6
- package/bun.lock +6 -2
- package/docs/architecture/memory.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/actor-token-service.test.ts +5 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/call-controller.test.ts +78 -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__/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__/guardian-actions-endpoint.test.ts +7 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
- 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 +4 -4
- 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 +50 -47
- package/src/__tests__/relay-server.test.ts +71 -0
- package/src/__tests__/send-endpoint-busy.test.ts +6 -0
- 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 +9 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +8 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +22 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +4 -4
- 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-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 +13 -13
- 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/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-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 +8 -0
- package/src/memory/guardian-bindings.ts +4 -0
- package/src/memory/job-handlers/backfill.ts +2 -9
- 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 +2 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema.ts +3 -0
- package/src/notifications/copy-composer.ts +2 -2
- package/src/runtime/access-request-helper.ts +43 -28
- package/src/runtime/actor-trust-resolver.ts +19 -14
- package/src/runtime/channel-guardian-service.ts +6 -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-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 +1 -0
- package/src/runtime/routes/inbound-conversation.ts +7 -7
- package/src/runtime/routes/inbound-message-handler.ts +105 -94
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/util/logger.ts +10 -0
- package/src/daemon/call-pointer-generators.ts +0 -59
|
@@ -22,6 +22,27 @@ mock.module('../util/logger.js', () => ({
|
|
|
22
22
|
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
+
// Mock config loader and feature flags to avoid filesystem reads on CI.
|
|
26
|
+
// getConfig returns a minimal config; all feature flags default to enabled.
|
|
27
|
+
mock.module('../config/loader.js', () => ({
|
|
28
|
+
getConfig: () => ({}),
|
|
29
|
+
loadConfig: () => ({}),
|
|
30
|
+
applyNestedDefaults: (c: unknown) => c,
|
|
31
|
+
saveConfig: () => {},
|
|
32
|
+
invalidateConfigCache: () => {},
|
|
33
|
+
loadRawConfig: () => ({}),
|
|
34
|
+
saveRawConfig: () => {},
|
|
35
|
+
getNestedValue: () => undefined,
|
|
36
|
+
setNestedValue: () => {},
|
|
37
|
+
API_KEY_PROVIDERS: [],
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
mock.module('../config/assistant-feature-flags.js', () => ({
|
|
41
|
+
isAssistantFeatureFlagEnabled: () => true,
|
|
42
|
+
loadDefaultsRegistry: () => ({}),
|
|
43
|
+
getFeatureFlagDefault: () => true,
|
|
44
|
+
}));
|
|
45
|
+
|
|
25
46
|
// Skill catalog: returns a configurable list of fake skills
|
|
26
47
|
let catalogSkillIds: string[] = [];
|
|
27
48
|
mock.module('../config/skills.js', () => ({
|
|
@@ -35,6 +56,21 @@ mock.module('../config/skills.js', () => ({
|
|
|
35
56
|
bundled: false,
|
|
36
57
|
userInvocable: false,
|
|
37
58
|
})),
|
|
59
|
+
// Needed by transitive deps (skill-state.ts imports this)
|
|
60
|
+
checkSkillRequirements: () => ({ eligible: true, missing: {} }),
|
|
61
|
+
getSkillsDir: () => '/tmp/fake-skills',
|
|
62
|
+
getBundledSkillsDir: () => '/tmp/fake-bundled-skills',
|
|
63
|
+
resolveSkillSelector: () => ({ found: false }),
|
|
64
|
+
loadSkillBySelector: () => ({ found: false }),
|
|
65
|
+
readCachedSkillIcon: () => undefined,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Mock skill-state.js to break the transitive import chain — the benchmark
|
|
69
|
+
// only needs skillFlagKey and doesn't exercise resolveSkillStates.
|
|
70
|
+
mock.module('../config/skill-state.js', () => ({
|
|
71
|
+
skillFlagKey: (id: string) => `feature_flags.${id}.enabled`,
|
|
72
|
+
isSkillFeatureEnabled: () => true,
|
|
73
|
+
resolveSkillStates: () => [],
|
|
38
74
|
}));
|
|
39
75
|
|
|
40
76
|
mock.module('../skills/tool-manifest.js', () => ({
|
|
@@ -91,9 +127,37 @@ mock.module('../tools/skills/skill-tool-factory.js', () => ({
|
|
|
91
127
|
})),
|
|
92
128
|
}));
|
|
93
129
|
|
|
94
|
-
//
|
|
130
|
+
// node:fs mock — TOOLS.json always exists for fake skills.
|
|
131
|
+
// Must include ALL named exports that any transitive dep imports, otherwise
|
|
132
|
+
// Bun's module resolver throws "Export named '…' not found".
|
|
133
|
+
const statStub = () => ({ isSymbolicLink: () => false, isDirectory: () => false, isFile: () => true });
|
|
95
134
|
mock.module('node:fs', () => ({
|
|
96
|
-
existsSync: () =>
|
|
135
|
+
existsSync: (p: string) => typeof p === 'string' && p.endsWith('TOOLS.json'),
|
|
136
|
+
readFileSync: () => '',
|
|
137
|
+
lstatSync: statStub,
|
|
138
|
+
readdirSync: () => [],
|
|
139
|
+
realpathSync: (p: string) => p,
|
|
140
|
+
statSync: statStub,
|
|
141
|
+
statfsSync: statStub,
|
|
142
|
+
mkdirSync: () => undefined,
|
|
143
|
+
mkdtempSync: () => '/tmp/mock',
|
|
144
|
+
renameSync: () => undefined,
|
|
145
|
+
rmSync: () => undefined,
|
|
146
|
+
unlinkSync: () => undefined,
|
|
147
|
+
writeFileSync: () => undefined,
|
|
148
|
+
appendFileSync: () => undefined,
|
|
149
|
+
copyFileSync: () => undefined,
|
|
150
|
+
cpSync: () => undefined,
|
|
151
|
+
chmodSync: () => undefined,
|
|
152
|
+
symlinkSync: () => undefined,
|
|
153
|
+
utimesSync: () => undefined,
|
|
154
|
+
openSync: () => 0,
|
|
155
|
+
closeSync: () => undefined,
|
|
156
|
+
readSync: () => 0,
|
|
157
|
+
createWriteStream: () => ({ write: () => true, end: () => {}, on: () => {} }),
|
|
158
|
+
watch: () => ({ close: () => {} }),
|
|
159
|
+
Dirent: class {},
|
|
160
|
+
Stats: class {},
|
|
97
161
|
}));
|
|
98
162
|
|
|
99
163
|
const benchmarkRegistry = new Map<string, unknown>();
|
|
@@ -286,7 +286,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
|
|
|
286
286
|
expect(result.allowed).toBe(true);
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
-
test('undefined actor role (desktop
|
|
289
|
+
test('undefined actor role (desktop) bypasses grant check', async () => {
|
|
290
290
|
const toolName = 'bash';
|
|
291
291
|
const input = { command: 'deploy' };
|
|
292
292
|
|
|
@@ -162,7 +162,7 @@ function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
|
162
162
|
return {
|
|
163
163
|
externalUserId: 'guardian-1',
|
|
164
164
|
channel: 'telegram',
|
|
165
|
-
|
|
165
|
+
guardianPrincipalId: 'test-principal-id',
|
|
166
166
|
...overrides,
|
|
167
167
|
};
|
|
168
168
|
}
|
|
@@ -361,6 +361,7 @@ describe('applyCanonicalGuardianDecision / tool_grant_request', () => {
|
|
|
361
361
|
conversationId: 'conv-1',
|
|
362
362
|
requesterExternalUserId: 'requester-1',
|
|
363
363
|
guardianExternalUserId: 'guardian-1',
|
|
364
|
+
guardianPrincipalId: 'test-principal-id',
|
|
364
365
|
toolName: 'bash',
|
|
365
366
|
inputDigest: 'sha256:testdigest',
|
|
366
367
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -390,6 +391,7 @@ describe('applyCanonicalGuardianDecision / tool_grant_request', () => {
|
|
|
390
391
|
conversationId: 'conv-1',
|
|
391
392
|
requesterExternalUserId: 'requester-1',
|
|
392
393
|
guardianExternalUserId: 'guardian-1',
|
|
394
|
+
guardianPrincipalId: 'test-principal-id',
|
|
393
395
|
toolName: 'bash',
|
|
394
396
|
inputDigest: 'sha256:testdigest',
|
|
395
397
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -417,6 +419,7 @@ describe('applyCanonicalGuardianDecision / tool_grant_request', () => {
|
|
|
417
419
|
conversationId: 'conv-1',
|
|
418
420
|
requesterExternalUserId: 'requester-1',
|
|
419
421
|
guardianExternalUserId: 'guardian-1',
|
|
422
|
+
guardianPrincipalId: 'test-principal-id',
|
|
420
423
|
toolName: 'bash',
|
|
421
424
|
inputDigest: 'sha256:testdigest',
|
|
422
425
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -425,7 +428,7 @@ describe('applyCanonicalGuardianDecision / tool_grant_request', () => {
|
|
|
425
428
|
const result = await applyCanonicalGuardianDecision({
|
|
426
429
|
requestId: req.id,
|
|
427
430
|
action: 'approve_once',
|
|
428
|
-
actorContext: guardianActor({
|
|
431
|
+
actorContext: guardianActor({ guardianPrincipalId: 'imposter-principal' }),
|
|
429
432
|
});
|
|
430
433
|
|
|
431
434
|
expect(result.applied).toBe(false);
|
|
@@ -564,6 +567,7 @@ describe('inline wait-and-resume', () => {
|
|
|
564
567
|
conversationId: 'conv-1',
|
|
565
568
|
requesterExternalUserId: 'requester-1',
|
|
566
569
|
guardianExternalUserId: 'guardian-1',
|
|
570
|
+
guardianPrincipalId: 'test-principal-id',
|
|
567
571
|
toolName: 'bash',
|
|
568
572
|
inputDigest: 'sha256:waitgrant',
|
|
569
573
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -606,6 +610,7 @@ describe('inline wait-and-resume', () => {
|
|
|
606
610
|
conversationId: 'conv-1',
|
|
607
611
|
requesterExternalUserId: 'requester-1',
|
|
608
612
|
guardianExternalUserId: 'guardian-1',
|
|
613
|
+
guardianPrincipalId: 'test-principal-id',
|
|
609
614
|
toolName: 'bash',
|
|
610
615
|
inputDigest: 'sha256:denywait',
|
|
611
616
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -645,6 +650,7 @@ describe('inline wait-and-resume', () => {
|
|
|
645
650
|
conversationId: 'conv-1',
|
|
646
651
|
requesterExternalUserId: 'requester-1',
|
|
647
652
|
guardianExternalUserId: 'guardian-1',
|
|
653
|
+
guardianPrincipalId: 'test-principal-id',
|
|
648
654
|
toolName: 'bash',
|
|
649
655
|
inputDigest: 'sha256:timeoutwait',
|
|
650
656
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -679,6 +685,7 @@ describe('inline wait-and-resume', () => {
|
|
|
679
685
|
conversationId: 'conv-1',
|
|
680
686
|
requesterExternalUserId: 'requester-1',
|
|
681
687
|
guardianExternalUserId: 'guardian-1',
|
|
688
|
+
guardianPrincipalId: 'test-principal-id',
|
|
682
689
|
toolName: 'bash',
|
|
683
690
|
inputDigest: 'sha256:abortwait',
|
|
684
691
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -221,7 +221,7 @@ function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
|
221
221
|
return {
|
|
222
222
|
externalUserId: 'guardian-1',
|
|
223
223
|
channel: 'telegram',
|
|
224
|
-
|
|
224
|
+
guardianPrincipalId: 'test-principal-id',
|
|
225
225
|
...overrides,
|
|
226
226
|
};
|
|
227
227
|
}
|
|
@@ -381,6 +381,7 @@ describe('(b) prompt-path flow: confirmation_request bridges to guardian', () =>
|
|
|
381
381
|
conversationId: 'conv-bridge-1',
|
|
382
382
|
requesterExternalUserId: 'requester-1',
|
|
383
383
|
guardianExternalUserId: 'guardian-1',
|
|
384
|
+
guardianPrincipalId: 'test-principal-id',
|
|
384
385
|
toolName: 'bash',
|
|
385
386
|
status: 'pending',
|
|
386
387
|
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
|
@@ -419,6 +420,7 @@ describe('(b) prompt-path flow: confirmation_request bridges to guardian', () =>
|
|
|
419
420
|
conversationId: 'conv-unified-1',
|
|
420
421
|
requesterExternalUserId: 'requester-1',
|
|
421
422
|
guardianExternalUserId: 'guardian-1',
|
|
423
|
+
guardianPrincipalId: 'test-principal-id',
|
|
422
424
|
toolName: 'bash',
|
|
423
425
|
status: 'pending',
|
|
424
426
|
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
|
@@ -508,6 +510,7 @@ describe('(c) no-binding flow: trusted contact fails fast without guardian bindi
|
|
|
508
510
|
conversationId: 'conv-nobinding',
|
|
509
511
|
requesterExternalUserId: 'requester-1',
|
|
510
512
|
guardianExternalUserId: 'guardian-1',
|
|
513
|
+
guardianPrincipalId: 'test-principal-id',
|
|
511
514
|
toolName: 'bash',
|
|
512
515
|
status: 'pending',
|
|
513
516
|
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
|
@@ -607,6 +610,7 @@ describe('(d) unknown actor flow: fail-closed with no interactive approval', ()
|
|
|
607
610
|
conversationId: 'conv-unknown',
|
|
608
611
|
requesterExternalUserId: 'unknown-user',
|
|
609
612
|
guardianExternalUserId: 'guardian-1',
|
|
613
|
+
guardianPrincipalId: 'test-principal-id',
|
|
610
614
|
toolName: 'bash',
|
|
611
615
|
status: 'pending',
|
|
612
616
|
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
|
@@ -794,6 +798,7 @@ describe('(f) timeout/stale flow: stale guardian decision after inline wait time
|
|
|
794
798
|
requesterExternalUserId: 'requester-1',
|
|
795
799
|
requesterChatId: 'requester-chat-1',
|
|
796
800
|
guardianExternalUserId: 'guardian-1',
|
|
801
|
+
guardianPrincipalId: 'test-principal-id',
|
|
797
802
|
toolName: 'bash',
|
|
798
803
|
inputDigest: 'sha256:stale',
|
|
799
804
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -842,6 +847,7 @@ describe('(f) timeout/stale flow: stale guardian decision after inline wait time
|
|
|
842
847
|
requesterExternalUserId: 'requester-1',
|
|
843
848
|
requesterChatId: 'requester-chat-1',
|
|
844
849
|
guardianExternalUserId: 'guardian-1',
|
|
850
|
+
guardianPrincipalId: 'test-principal-id',
|
|
845
851
|
toolName: 'bash',
|
|
846
852
|
inputDigest: 'sha256:fresh',
|
|
847
853
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -974,6 +980,7 @@ describe('cross-milestone integration checks', () => {
|
|
|
974
980
|
conversationId: 'conv-consistency',
|
|
975
981
|
requesterExternalUserId: 'requester-1',
|
|
976
982
|
guardianExternalUserId: 'guardian-1',
|
|
983
|
+
guardianPrincipalId: 'test-principal-id',
|
|
977
984
|
toolName: 'bash',
|
|
978
985
|
status: 'pending',
|
|
979
986
|
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
|
@@ -125,12 +125,12 @@ function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
|
125
125
|
const body: Record<string, unknown> = {
|
|
126
126
|
sourceChannel: 'telegram',
|
|
127
127
|
interface: 'telegram',
|
|
128
|
-
|
|
128
|
+
conversationExternalId: 'chat-123',
|
|
129
129
|
externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
130
130
|
content: 'Hello',
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
actorExternalId: 'requester-user-456',
|
|
132
|
+
actorDisplayName: 'Alice Requester',
|
|
133
|
+
actorUsername: 'alice_req',
|
|
134
134
|
replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
|
|
135
135
|
...overrides,
|
|
136
136
|
};
|
|
@@ -192,9 +192,9 @@ describe('trusted contact lifecycle notification signals', () => {
|
|
|
192
192
|
|
|
193
193
|
// Guardian denies via callback button
|
|
194
194
|
const guardianReq = buildInboundRequest({
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
conversationExternalId: 'guardian-chat-789',
|
|
196
|
+
actorExternalId: 'guardian-user-789',
|
|
197
|
+
actorDisplayName: 'Guardian',
|
|
198
198
|
content: '',
|
|
199
199
|
callbackData: `apr:${testRequestId}:reject`,
|
|
200
200
|
});
|
|
@@ -267,9 +267,9 @@ describe('trusted contact lifecycle notification signals', () => {
|
|
|
267
267
|
|
|
268
268
|
// Guardian approves via callback button
|
|
269
269
|
const guardianReq = buildInboundRequest({
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
conversationExternalId: 'guardian-chat-789',
|
|
271
|
+
actorExternalId: 'guardian-user-789',
|
|
272
|
+
actorDisplayName: 'Guardian',
|
|
273
273
|
content: '',
|
|
274
274
|
callbackData: `apr:${testRequestId}:approve_once`,
|
|
275
275
|
});
|
|
@@ -340,9 +340,9 @@ describe('trusted contact lifecycle notification signals', () => {
|
|
|
340
340
|
|
|
341
341
|
// All guardian_decision signals include the approval ID in the dedupe key
|
|
342
342
|
const guardianReq = buildInboundRequest({
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
343
|
+
conversationExternalId: 'guardian-chat-789',
|
|
344
|
+
actorExternalId: 'guardian-user-789',
|
|
345
|
+
actorDisplayName: 'Guardian',
|
|
346
346
|
content: '',
|
|
347
347
|
callbackData: `apr:${testRequestId}:reject`,
|
|
348
348
|
});
|
|
@@ -389,8 +389,8 @@ describe('trusted contact activated notification signal', () => {
|
|
|
389
389
|
// Requester enters the verification code
|
|
390
390
|
const verifyReq = buildInboundRequest({
|
|
391
391
|
content: session.secret,
|
|
392
|
-
|
|
393
|
-
|
|
392
|
+
conversationExternalId: 'chat-123',
|
|
393
|
+
actorExternalId: 'requester-user-456',
|
|
394
394
|
});
|
|
395
395
|
|
|
396
396
|
await handleChannelInbound(verifyReq, undefined, TEST_BEARER_TOKEN);
|
|
@@ -449,9 +449,9 @@ describe('trusted contact activated notification signal', () => {
|
|
|
449
449
|
|
|
450
450
|
const verifyReq = buildInboundRequest({
|
|
451
451
|
content: session.secret,
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
452
|
+
conversationExternalId: 'chat-123',
|
|
453
|
+
actorExternalId: 'requester-user-456',
|
|
454
|
+
actorDisplayName: 'Noa Flaherty',
|
|
455
455
|
});
|
|
456
456
|
|
|
457
457
|
await handleChannelInbound(verifyReq, undefined, TEST_BEARER_TOKEN);
|
|
@@ -476,8 +476,8 @@ describe('trusted contact activated notification signal', () => {
|
|
|
476
476
|
// "Guardian" enters the verification code
|
|
477
477
|
const verifyReq = buildInboundRequest({
|
|
478
478
|
content: secret,
|
|
479
|
-
|
|
480
|
-
|
|
479
|
+
conversationExternalId: 'guardian-chat-new',
|
|
480
|
+
actorExternalId: 'guardian-user-new',
|
|
481
481
|
});
|
|
482
482
|
|
|
483
483
|
await handleChannelInbound(verifyReq, undefined, TEST_BEARER_TOKEN);
|
|
@@ -520,8 +520,8 @@ describe('trusted contact activated notification signal', () => {
|
|
|
520
520
|
|
|
521
521
|
const verifyReq = buildInboundRequest({
|
|
522
522
|
content: session.secret,
|
|
523
|
-
|
|
524
|
-
|
|
523
|
+
conversationExternalId: 'chat-123',
|
|
524
|
+
actorExternalId: 'requester-user-456',
|
|
525
525
|
});
|
|
526
526
|
|
|
527
527
|
await handleChannelInbound(verifyReq, undefined, TEST_BEARER_TOKEN);
|
|
@@ -160,12 +160,12 @@ function buildInboundRequest(
|
|
|
160
160
|
const body: Record<string, unknown> = {
|
|
161
161
|
sourceChannel: config.channel,
|
|
162
162
|
interface: config.channel,
|
|
163
|
-
|
|
163
|
+
conversationExternalId: config.externalChatId,
|
|
164
164
|
externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
165
165
|
content: 'Hello, can I use this assistant?',
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
actorExternalId: config.senderExternalUserId,
|
|
167
|
+
actorDisplayName: 'Test Requester',
|
|
168
|
+
actorUsername: 'test_requester',
|
|
169
169
|
replyCallbackUrl: `http://localhost:7830${config.deliverEndpoint}`,
|
|
170
170
|
...overrides,
|
|
171
171
|
};
|
|
@@ -158,8 +158,8 @@ describe('trusted contact verification → member activation', () => {
|
|
|
158
158
|
const trust = resolveActorTrust({
|
|
159
159
|
assistantId: 'self',
|
|
160
160
|
sourceChannel: 'telegram',
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
conversationExternalId: 'requester-chat-jeff',
|
|
162
|
+
actorExternalId: 'requester-user-jeff',
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
expect(trust.trustClass).toBe('trusted_contact');
|
|
@@ -184,10 +184,10 @@ describe('trusted contact verification → member activation', () => {
|
|
|
184
184
|
const trust = resolveActorTrust({
|
|
185
185
|
assistantId: 'self',
|
|
186
186
|
sourceChannel: 'telegram',
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
conversationExternalId: 'requester-chat-jeff-priority',
|
|
188
|
+
actorExternalId: 'requester-user-jeff-priority',
|
|
189
|
+
actorUsername: 'jeffrey_telegram',
|
|
190
|
+
actorDisplayName: 'Jeffrey',
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
expect(trust.trustClass).toBe('trusted_contact');
|
|
@@ -216,10 +216,10 @@ describe('trusted contact verification → member activation', () => {
|
|
|
216
216
|
const trust = resolveActorTrust({
|
|
217
217
|
assistantId: 'self',
|
|
218
218
|
sourceChannel: 'telegram',
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
conversationExternalId: 'shared-group-chat',
|
|
220
|
+
actorExternalId: 'actual-sender-in-group',
|
|
221
|
+
actorUsername: 'actual_sender_handle',
|
|
222
|
+
actorDisplayName: 'Actual Sender',
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
// The member record returned by findMember matched on chatId but belongs
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* 9. Kind-specific resolver dispatch via the resolver registry
|
|
19
19
|
*
|
|
20
20
|
* Security invariants enforced here:
|
|
21
|
-
* - Decision
|
|
21
|
+
* - Decision authorization is purely principal-based:
|
|
22
|
+
* actor.guardianPrincipalId === request.guardianPrincipalId (strict equality)
|
|
22
23
|
* - Decisions are first-response-wins (CAS-like stale protection)
|
|
23
24
|
* - `approve_always` is rejected/downgraded for guardian-on-behalf requests
|
|
24
25
|
* - Scoped grant minting only on explicit approve for requests with tool metadata
|
|
@@ -352,44 +353,46 @@ export async function applyCanonicalGuardianDecision(
|
|
|
352
353
|
return { applied: false, reason: 'invalid_action', detail: `invalid action: ${action}` };
|
|
353
354
|
}
|
|
354
355
|
|
|
355
|
-
// 2c.
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// non-guardian actor can never approve an unbound request.
|
|
361
|
-
if (
|
|
362
|
-
!actorContext.isTrusted &&
|
|
363
|
-
request.kind === 'tool_approval' &&
|
|
364
|
-
!request.guardianExternalUserId
|
|
365
|
-
) {
|
|
356
|
+
// 2c. Principal-based authorization: actor.guardianPrincipalId must match
|
|
357
|
+
// request.guardianPrincipalId for any applied decision. This is the single
|
|
358
|
+
// authorization gate — principal identity must always match.
|
|
359
|
+
|
|
360
|
+
if (!request.guardianPrincipalId) {
|
|
366
361
|
log.warn(
|
|
367
362
|
{
|
|
368
|
-
event: '
|
|
363
|
+
event: 'canonical_decision_missing_request_principal',
|
|
369
364
|
requestId,
|
|
370
365
|
kind: request.kind,
|
|
371
366
|
sourceType: request.sourceType,
|
|
372
367
|
},
|
|
373
|
-
'Canonical
|
|
368
|
+
'Canonical request missing guardianPrincipalId; rejecting decision',
|
|
369
|
+
);
|
|
370
|
+
return { applied: false, reason: 'identity_mismatch', detail: 'request missing guardianPrincipalId' };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!actorContext.guardianPrincipalId) {
|
|
374
|
+
log.warn(
|
|
375
|
+
{
|
|
376
|
+
event: 'canonical_decision_missing_actor_principal',
|
|
377
|
+
requestId,
|
|
378
|
+
actorChannel: actorContext.channel,
|
|
379
|
+
},
|
|
380
|
+
'Actor missing guardianPrincipalId; rejecting decision',
|
|
374
381
|
);
|
|
375
|
-
return { applied: false, reason: 'identity_mismatch', detail: 'missing
|
|
382
|
+
return { applied: false, reason: 'identity_mismatch', detail: 'actor missing guardianPrincipalId' };
|
|
376
383
|
}
|
|
377
384
|
|
|
378
|
-
if (
|
|
379
|
-
request.guardianExternalUserId &&
|
|
380
|
-
!actorContext.isTrusted &&
|
|
381
|
-
actorContext.externalUserId !== request.guardianExternalUserId
|
|
382
|
-
) {
|
|
385
|
+
if (actorContext.guardianPrincipalId !== request.guardianPrincipalId) {
|
|
383
386
|
log.warn(
|
|
384
387
|
{
|
|
385
|
-
event: '
|
|
388
|
+
event: 'canonical_decision_principal_mismatch',
|
|
386
389
|
requestId,
|
|
387
|
-
|
|
388
|
-
|
|
390
|
+
expectedPrincipal: request.guardianPrincipalId,
|
|
391
|
+
actualPrincipal: actorContext.guardianPrincipalId,
|
|
389
392
|
},
|
|
390
|
-
'Actor
|
|
393
|
+
'Actor principal does not match request principal',
|
|
391
394
|
);
|
|
392
|
-
return { applied: false, reason: 'identity_mismatch' };
|
|
395
|
+
return { applied: false, reason: 'identity_mismatch', detail: 'principal mismatch' };
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
// 2d. Check expiry
|
|
@@ -416,6 +419,7 @@ export async function applyCanonicalGuardianDecision(
|
|
|
416
419
|
status: targetStatus,
|
|
417
420
|
answerText: userText,
|
|
418
421
|
decidedByExternalUserId: actorContext.externalUserId,
|
|
422
|
+
decidedByPrincipalId: actorContext.guardianPrincipalId,
|
|
419
423
|
});
|
|
420
424
|
|
|
421
425
|
if (!resolved) {
|
|
@@ -39,12 +39,12 @@ const log = getLogger('guardian-request-resolvers');
|
|
|
39
39
|
|
|
40
40
|
/** Actor context for the entity making the decision. */
|
|
41
41
|
export interface ActorContext {
|
|
42
|
-
/** External user ID of the deciding actor (undefined for desktop
|
|
42
|
+
/** External user ID of the deciding actor (undefined for desktop actors). */
|
|
43
43
|
externalUserId: string | undefined;
|
|
44
44
|
/** Channel the decision arrived on. */
|
|
45
45
|
channel: string;
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/** Principal ID for authorization — must match the request's guardianPrincipalId. */
|
|
47
|
+
guardianPrincipalId: string | undefined;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** The decision being applied. */
|
|
@@ -385,7 +385,9 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
385
385
|
return {
|
|
386
386
|
ok: true,
|
|
387
387
|
applied: true,
|
|
388
|
-
|
|
388
|
+
// Desktop actors (vellum channel) receive inline reply text; channel
|
|
389
|
+
// actors get replies delivered via the channel delivery context.
|
|
390
|
+
...(ctx.actor.channel === 'vellum' ? { guardianReplyText: `Access denied for ${requesterLabel}.` } : {}),
|
|
389
391
|
};
|
|
390
392
|
}
|
|
391
393
|
|
|
@@ -539,7 +541,9 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
539
541
|
return {
|
|
540
542
|
ok: true,
|
|
541
543
|
applied: true,
|
|
542
|
-
|
|
544
|
+
// Desktop actors (vellum channel) receive inline reply text; channel
|
|
545
|
+
// actors get replies delivered via the channel delivery context.
|
|
546
|
+
...(ctx.actor.channel === 'vellum' ? { guardianReplyText: verificationReplyText } : {}),
|
|
543
547
|
};
|
|
544
548
|
},
|
|
545
549
|
};
|