@vellumai/assistant 0.4.2 → 0.4.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/.env.example +3 -0
- package/ARCHITECTURE.md +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -86
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +1475 -33
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -100,6 +100,9 @@ interface TestSession {
|
|
|
100
100
|
setAssistantId: (assistantId: string) => void;
|
|
101
101
|
setGuardianContext: (ctx: unknown) => void;
|
|
102
102
|
setCommandIntent: (intent: unknown) => void;
|
|
103
|
+
updateClient: (sendToClient: (msg: ServerMessage) => void, hasNoClient?: boolean) => void;
|
|
104
|
+
emitActivityState: (...args: unknown[]) => void;
|
|
105
|
+
emitConfirmationStateChanged: (...args: unknown[]) => void;
|
|
103
106
|
processMessage: (...args: unknown[]) => Promise<string>;
|
|
104
107
|
}
|
|
105
108
|
|
|
@@ -153,6 +156,9 @@ function makeSession(overrides: Partial<TestSession> = {}): TestSession {
|
|
|
153
156
|
setAssistantId: () => {},
|
|
154
157
|
setGuardianContext: () => {},
|
|
155
158
|
setCommandIntent: () => {},
|
|
159
|
+
updateClient: () => {},
|
|
160
|
+
emitActivityState: () => {},
|
|
161
|
+
emitConfirmationStateChanged: () => {},
|
|
156
162
|
processMessage: async () => 'msg-id',
|
|
157
163
|
...overrides,
|
|
158
164
|
};
|
|
@@ -411,6 +417,53 @@ describe('handleUserMessage pending-confirmation reply interception', () => {
|
|
|
411
417
|
expect(sent.some((event) => event.type === 'confirmation_request')).toBe(true);
|
|
412
418
|
});
|
|
413
419
|
|
|
420
|
+
test('registers IPC confirmation events emitted via session sender (prompter path)', async () => {
|
|
421
|
+
let currentSender: (msg: ServerMessage) => void = () => {};
|
|
422
|
+
const session = makeSession({
|
|
423
|
+
hasAnyPendingConfirmation: () => false,
|
|
424
|
+
enqueueMessage: mock(() => ({ queued: false, requestId: 'direct-id' })),
|
|
425
|
+
updateClient: (sendToClient: (msg: ServerMessage) => void) => {
|
|
426
|
+
currentSender = sendToClient;
|
|
427
|
+
},
|
|
428
|
+
processMessage: async () => {
|
|
429
|
+
currentSender({
|
|
430
|
+
type: 'confirmation_request',
|
|
431
|
+
requestId: 'req-prompter-1',
|
|
432
|
+
toolName: 'call_start',
|
|
433
|
+
input: { phone_number: '+18084436762' },
|
|
434
|
+
riskLevel: 'high',
|
|
435
|
+
executionTarget: 'host',
|
|
436
|
+
allowlistOptions: [],
|
|
437
|
+
scopeOptions: [],
|
|
438
|
+
persistentDecisionsAllowed: false,
|
|
439
|
+
} as ServerMessage);
|
|
440
|
+
return 'msg-id';
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
const { ctx, sent } = createContext(session);
|
|
444
|
+
|
|
445
|
+
await handleUserMessage(makeMessage('please call now'), {} as net.Socket, ctx);
|
|
446
|
+
|
|
447
|
+
expect(registerMock).toHaveBeenCalledWith(
|
|
448
|
+
'req-prompter-1',
|
|
449
|
+
expect.objectContaining({
|
|
450
|
+
conversationId: 'conv-1',
|
|
451
|
+
kind: 'confirmation',
|
|
452
|
+
session,
|
|
453
|
+
}),
|
|
454
|
+
);
|
|
455
|
+
expect(createCanonicalGuardianRequestMock).toHaveBeenCalledWith(
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
id: 'req-prompter-1',
|
|
458
|
+
kind: 'tool_approval',
|
|
459
|
+
sourceType: 'desktop',
|
|
460
|
+
sourceChannel: 'vellum',
|
|
461
|
+
conversationId: 'conv-1',
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
expect(sent.some((event) => event.type === 'confirmation_request')).toBe(true);
|
|
465
|
+
});
|
|
466
|
+
|
|
414
467
|
test('syncs canonical status to approved for IPC allow decisions', () => {
|
|
415
468
|
const session = {
|
|
416
469
|
hasPendingConfirmation: (requestId: string) => requestId === 'req-confirm-allow',
|
|
@@ -433,6 +486,8 @@ describe('handleUserMessage pending-confirmation reply interception', () => {
|
|
|
433
486
|
'always_allow',
|
|
434
487
|
undefined,
|
|
435
488
|
undefined,
|
|
489
|
+
undefined,
|
|
490
|
+
{ source: 'button' },
|
|
436
491
|
]);
|
|
437
492
|
expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledWith(
|
|
438
493
|
'req-confirm-allow',
|
|
@@ -32,6 +32,8 @@ mock.module('../tools/browser/browser-manager.js', () => {
|
|
|
32
32
|
getOrCreateSessionPage: getOrCreateSessionPageMock,
|
|
33
33
|
clearSnapshotMap: mock(() => {}),
|
|
34
34
|
supportsRouteInterception: true,
|
|
35
|
+
isInteractive: () => false,
|
|
36
|
+
positionWindowSidebar: () => {},
|
|
35
37
|
},
|
|
36
38
|
};
|
|
37
39
|
});
|
|
@@ -28,7 +28,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
28
28
|
getDbPath: () => join(testDir, 'test.db'),
|
|
29
29
|
getLogPath: () => join(testDir, 'test.log'),
|
|
30
30
|
ensureDataDir: () => {},
|
|
31
|
-
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
32
31
|
readHttpToken: () => 'test-bearer-token',
|
|
33
32
|
}));
|
|
34
33
|
|
|
@@ -492,6 +492,8 @@ describe('voice invite HTTP routes', () => {
|
|
|
492
492
|
body: JSON.stringify({
|
|
493
493
|
sourceChannel: 'voice',
|
|
494
494
|
expectedExternalUserId: '+15551234567',
|
|
495
|
+
friendName: 'Alice',
|
|
496
|
+
guardianName: 'Bob',
|
|
495
497
|
maxUses: 3,
|
|
496
498
|
}),
|
|
497
499
|
});
|
|
@@ -514,6 +516,9 @@ describe('voice invite HTTP routes', () => {
|
|
|
514
516
|
expect(invite.voiceCodeDigits).toBe(6);
|
|
515
517
|
// expectedExternalUserId should be recorded
|
|
516
518
|
expect(invite.expectedExternalUserId).toBe('+15551234567');
|
|
519
|
+
// friendName and guardianName should be recorded
|
|
520
|
+
expect(invite.friendName).toBe('Alice');
|
|
521
|
+
expect(invite.guardianName).toBe('Bob');
|
|
517
522
|
});
|
|
518
523
|
|
|
519
524
|
test('voice invite creation requires expectedExternalUserId', async () => {
|
|
@@ -522,6 +527,8 @@ describe('voice invite HTTP routes', () => {
|
|
|
522
527
|
headers: { 'Content-Type': 'application/json' },
|
|
523
528
|
body: JSON.stringify({
|
|
524
529
|
sourceChannel: 'voice',
|
|
530
|
+
friendName: 'Alice',
|
|
531
|
+
guardianName: 'Bob',
|
|
525
532
|
}),
|
|
526
533
|
});
|
|
527
534
|
|
|
@@ -540,6 +547,8 @@ describe('voice invite HTTP routes', () => {
|
|
|
540
547
|
body: JSON.stringify({
|
|
541
548
|
sourceChannel: 'voice',
|
|
542
549
|
expectedExternalUserId: 'not-a-phone-number',
|
|
550
|
+
friendName: 'Alice',
|
|
551
|
+
guardianName: 'Bob',
|
|
543
552
|
}),
|
|
544
553
|
});
|
|
545
554
|
|
|
@@ -551,6 +560,44 @@ describe('voice invite HTTP routes', () => {
|
|
|
551
560
|
expect(body.error).toContain('E.164');
|
|
552
561
|
});
|
|
553
562
|
|
|
563
|
+
test('voice invite creation requires friendName', async () => {
|
|
564
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
565
|
+
method: 'POST',
|
|
566
|
+
headers: { 'Content-Type': 'application/json' },
|
|
567
|
+
body: JSON.stringify({
|
|
568
|
+
sourceChannel: 'voice',
|
|
569
|
+
expectedExternalUserId: '+15551234567',
|
|
570
|
+
guardianName: 'Bob',
|
|
571
|
+
}),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const res = await handleCreateInvite(req);
|
|
575
|
+
const body = await res.json() as Record<string, unknown>;
|
|
576
|
+
|
|
577
|
+
expect(res.status).toBe(400);
|
|
578
|
+
expect(body.ok).toBe(false);
|
|
579
|
+
expect(body.error).toContain('friendName');
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('voice invite creation requires guardianName', async () => {
|
|
583
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
584
|
+
method: 'POST',
|
|
585
|
+
headers: { 'Content-Type': 'application/json' },
|
|
586
|
+
body: JSON.stringify({
|
|
587
|
+
sourceChannel: 'voice',
|
|
588
|
+
expectedExternalUserId: '+15551234567',
|
|
589
|
+
friendName: 'Alice',
|
|
590
|
+
}),
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const res = await handleCreateInvite(req);
|
|
594
|
+
const body = await res.json() as Record<string, unknown>;
|
|
595
|
+
|
|
596
|
+
expect(res.status).toBe(400);
|
|
597
|
+
expect(body.ok).toBe(false);
|
|
598
|
+
expect(body.error).toContain('guardianName');
|
|
599
|
+
});
|
|
600
|
+
|
|
554
601
|
test('voiceCodeDigits is always 6 — custom values are ignored', async () => {
|
|
555
602
|
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
556
603
|
method: 'POST',
|
|
@@ -558,6 +605,8 @@ describe('voice invite HTTP routes', () => {
|
|
|
558
605
|
body: JSON.stringify({
|
|
559
606
|
sourceChannel: 'voice',
|
|
560
607
|
expectedExternalUserId: '+15551234567',
|
|
608
|
+
friendName: 'Alice',
|
|
609
|
+
guardianName: 'Bob',
|
|
561
610
|
voiceCodeDigits: 8,
|
|
562
611
|
}),
|
|
563
612
|
});
|
|
@@ -579,6 +628,8 @@ describe('voice invite HTTP routes', () => {
|
|
|
579
628
|
body: JSON.stringify({
|
|
580
629
|
sourceChannel: 'voice',
|
|
581
630
|
expectedExternalUserId: '+15551234567',
|
|
631
|
+
friendName: 'Alice',
|
|
632
|
+
guardianName: 'Bob',
|
|
582
633
|
}),
|
|
583
634
|
});
|
|
584
635
|
|
|
@@ -600,6 +651,8 @@ describe('voice invite HTTP routes', () => {
|
|
|
600
651
|
body: JSON.stringify({
|
|
601
652
|
sourceChannel: 'voice',
|
|
602
653
|
expectedExternalUserId: '+15551234567',
|
|
654
|
+
friendName: 'Alice',
|
|
655
|
+
guardianName: 'Bob',
|
|
603
656
|
maxUses: 1,
|
|
604
657
|
}),
|
|
605
658
|
}));
|
|
@@ -648,6 +701,8 @@ describe('voice invite HTTP routes', () => {
|
|
|
648
701
|
body: JSON.stringify({
|
|
649
702
|
sourceChannel: 'voice',
|
|
650
703
|
expectedExternalUserId: '+15551234567',
|
|
704
|
+
friendName: 'Alice',
|
|
705
|
+
guardianName: 'Bob',
|
|
651
706
|
maxUses: 1,
|
|
652
707
|
}),
|
|
653
708
|
}));
|
|
@@ -422,16 +422,6 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
|
|
|
422
422
|
type: 'telegram_config',
|
|
423
423
|
action: 'get',
|
|
424
424
|
},
|
|
425
|
-
twilio_config: {
|
|
426
|
-
type: 'twilio_config',
|
|
427
|
-
action: 'get',
|
|
428
|
-
},
|
|
429
|
-
channel_readiness: {
|
|
430
|
-
type: 'channel_readiness',
|
|
431
|
-
action: 'get',
|
|
432
|
-
channel: 'sms',
|
|
433
|
-
includeRemote: true,
|
|
434
|
-
},
|
|
435
425
|
guardian_verification: {
|
|
436
426
|
type: 'guardian_verification',
|
|
437
427
|
action: 'create_challenge',
|
|
@@ -823,6 +813,24 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
823
813
|
sandboxed: false,
|
|
824
814
|
sessionId: 'sess-001',
|
|
825
815
|
},
|
|
816
|
+
confirmation_state_changed: {
|
|
817
|
+
type: 'confirmation_state_changed',
|
|
818
|
+
sessionId: 'sess-001',
|
|
819
|
+
requestId: 'req-002',
|
|
820
|
+
state: 'approved',
|
|
821
|
+
source: 'inline_nl',
|
|
822
|
+
causedByRequestId: 'req-003',
|
|
823
|
+
decisionText: 'approve',
|
|
824
|
+
},
|
|
825
|
+
assistant_activity_state: {
|
|
826
|
+
type: 'assistant_activity_state',
|
|
827
|
+
sessionId: 'sess-001',
|
|
828
|
+
activityVersion: 1,
|
|
829
|
+
phase: 'thinking',
|
|
830
|
+
anchor: 'assistant_turn',
|
|
831
|
+
requestId: 'req-003',
|
|
832
|
+
reason: 'message_dequeued',
|
|
833
|
+
},
|
|
826
834
|
message_complete: {
|
|
827
835
|
type: 'message_complete',
|
|
828
836
|
sessionId: 'sess-001',
|
|
@@ -1446,47 +1454,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1446
1454
|
connected: true,
|
|
1447
1455
|
hasWebhookSecret: true,
|
|
1448
1456
|
},
|
|
1449
|
-
twilio_config_response: {
|
|
1450
|
-
type: 'twilio_config_response',
|
|
1451
|
-
success: true,
|
|
1452
|
-
hasCredentials: true,
|
|
1453
|
-
phoneNumber: '+15551234567',
|
|
1454
|
-
compliance: {
|
|
1455
|
-
numberType: 'toll_free',
|
|
1456
|
-
verificationSid: 'TF_VER_001',
|
|
1457
|
-
verificationStatus: 'TWILIO_APPROVED',
|
|
1458
|
-
},
|
|
1459
|
-
testResult: {
|
|
1460
|
-
messageSid: 'SM-test-001',
|
|
1461
|
-
to: '+15559876543',
|
|
1462
|
-
initialStatus: 'queued',
|
|
1463
|
-
finalStatus: 'delivered',
|
|
1464
|
-
},
|
|
1465
|
-
diagnostics: {
|
|
1466
|
-
readiness: { ready: true, issues: [] },
|
|
1467
|
-
compliance: { status: 'TWILIO_APPROVED', detail: 'Toll-free verification: TWILIO_APPROVED' },
|
|
1468
|
-
overallStatus: 'healthy',
|
|
1469
|
-
actionItems: [],
|
|
1470
|
-
},
|
|
1471
|
-
},
|
|
1472
|
-
channel_readiness_response: {
|
|
1473
|
-
type: 'channel_readiness_response',
|
|
1474
|
-
success: true,
|
|
1475
|
-
snapshots: [
|
|
1476
|
-
{
|
|
1477
|
-
channel: 'sms',
|
|
1478
|
-
ready: false,
|
|
1479
|
-
checkedAt: 1700000000000,
|
|
1480
|
-
stale: false,
|
|
1481
|
-
reasons: [{ code: 'twilio_credentials', text: 'Twilio credentials are not configured' }],
|
|
1482
|
-
localChecks: [
|
|
1483
|
-
{ name: 'twilio_credentials', passed: false, message: 'Twilio credentials are not configured' },
|
|
1484
|
-
{ name: 'phone_number', passed: true, message: 'Phone number is assigned' },
|
|
1485
|
-
{ name: 'ingress', passed: true, message: 'Public ingress URL is configured' },
|
|
1486
|
-
],
|
|
1487
|
-
},
|
|
1488
|
-
],
|
|
1489
|
-
},
|
|
1490
1457
|
guardian_verification_response: {
|
|
1491
1458
|
type: 'guardian_verification_response',
|
|
1492
1459
|
success: true,
|
|
@@ -30,7 +30,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
30
30
|
getDbPath: () => join(testDir, 'test.db'),
|
|
31
31
|
getLogPath: () => join(testDir, 'test.log'),
|
|
32
32
|
ensureDataDir: () => {},
|
|
33
|
-
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
34
33
|
readHttpToken: () => 'test-bearer-token',
|
|
35
34
|
}));
|
|
36
35
|
|
|
@@ -59,16 +58,23 @@ mock.module('../config/env.js', () => ({
|
|
|
59
58
|
|
|
60
59
|
// Track emitNotificationSignal calls
|
|
61
60
|
const emitSignalCalls: Array<Record<string, unknown>> = [];
|
|
61
|
+
let mockEmitResult: {
|
|
62
|
+
signalId: string;
|
|
63
|
+
deduplicated: boolean;
|
|
64
|
+
dispatched: boolean;
|
|
65
|
+
reason: string;
|
|
66
|
+
deliveryResults: Array<Record<string, unknown>>;
|
|
67
|
+
} = {
|
|
68
|
+
signalId: 'mock-signal-id',
|
|
69
|
+
deduplicated: false,
|
|
70
|
+
dispatched: true,
|
|
71
|
+
reason: 'mock',
|
|
72
|
+
deliveryResults: [],
|
|
73
|
+
};
|
|
62
74
|
mock.module('../notifications/emit-signal.js', () => ({
|
|
63
75
|
emitNotificationSignal: async (params: Record<string, unknown>) => {
|
|
64
76
|
emitSignalCalls.push(params);
|
|
65
|
-
return
|
|
66
|
-
signalId: 'mock-signal-id',
|
|
67
|
-
deduplicated: false,
|
|
68
|
-
dispatched: true,
|
|
69
|
-
reason: 'mock',
|
|
70
|
-
deliveryResults: [],
|
|
71
|
-
};
|
|
77
|
+
return mockEmitResult;
|
|
72
78
|
},
|
|
73
79
|
}));
|
|
74
80
|
|
|
@@ -80,7 +86,10 @@ mock.module('../runtime/gateway-client.js', () => ({
|
|
|
80
86
|
},
|
|
81
87
|
}));
|
|
82
88
|
|
|
83
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
listCanonicalGuardianDeliveries,
|
|
91
|
+
listCanonicalGuardianRequests,
|
|
92
|
+
} from '../memory/canonical-guardian-store.js';
|
|
84
93
|
import {
|
|
85
94
|
createBinding,
|
|
86
95
|
} from '../memory/channel-guardian-store.js';
|
|
@@ -112,6 +121,17 @@ function resetState(): void {
|
|
|
112
121
|
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
113
122
|
emitSignalCalls.length = 0;
|
|
114
123
|
deliverReplyCalls.length = 0;
|
|
124
|
+
mockEmitResult = {
|
|
125
|
+
signalId: 'mock-signal-id',
|
|
126
|
+
deduplicated: false,
|
|
127
|
+
dispatched: true,
|
|
128
|
+
reason: 'mock',
|
|
129
|
+
deliveryResults: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function flushAsyncAccessRequestBookkeeping(): Promise<void> {
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
@@ -437,6 +457,34 @@ describe('access-request-helper unit tests', () => {
|
|
|
437
457
|
expect(payload.guardianBindingChannel).toBe('telegram');
|
|
438
458
|
});
|
|
439
459
|
|
|
460
|
+
test('notifyGuardianOfAccessRequest for voice channel includes senderName in contextPayload', () => {
|
|
461
|
+
const result = notifyGuardianOfAccessRequest({
|
|
462
|
+
canonicalAssistantId: 'self',
|
|
463
|
+
sourceChannel: 'voice',
|
|
464
|
+
externalChatId: '+15559998888',
|
|
465
|
+
senderExternalUserId: '+15559998888',
|
|
466
|
+
senderName: 'Alice Caller',
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(result.notified).toBe(true);
|
|
470
|
+
expect(emitSignalCalls.length).toBe(1);
|
|
471
|
+
|
|
472
|
+
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
473
|
+
expect(payload.sourceChannel).toBe('voice');
|
|
474
|
+
expect(payload.senderName).toBe('Alice Caller');
|
|
475
|
+
expect(payload.senderExternalUserId).toBe('+15559998888');
|
|
476
|
+
expect(payload.senderIdentifier).toBe('Alice Caller');
|
|
477
|
+
|
|
478
|
+
// Canonical request should exist
|
|
479
|
+
const pending = listCanonicalGuardianRequests({
|
|
480
|
+
status: 'pending',
|
|
481
|
+
requesterExternalUserId: '+15559998888',
|
|
482
|
+
sourceChannel: 'voice',
|
|
483
|
+
kind: 'access_request',
|
|
484
|
+
});
|
|
485
|
+
expect(pending.length).toBe(1);
|
|
486
|
+
});
|
|
487
|
+
|
|
440
488
|
test('notifyGuardianOfAccessRequest includes requestCode in contextPayload', () => {
|
|
441
489
|
const result = notifyGuardianOfAccessRequest({
|
|
442
490
|
canonicalAssistantId: 'self',
|
|
@@ -454,4 +502,106 @@ describe('access-request-helper unit tests', () => {
|
|
|
454
502
|
expect(typeof payload.requestCode).toBe('string');
|
|
455
503
|
expect((payload.requestCode as string).length).toBe(6);
|
|
456
504
|
});
|
|
505
|
+
|
|
506
|
+
test('notifyGuardianOfAccessRequest includes previousMemberStatus in contextPayload', () => {
|
|
507
|
+
const result = notifyGuardianOfAccessRequest({
|
|
508
|
+
canonicalAssistantId: 'self',
|
|
509
|
+
sourceChannel: 'telegram',
|
|
510
|
+
externalChatId: 'chat-123',
|
|
511
|
+
senderExternalUserId: 'revoked-user',
|
|
512
|
+
senderName: 'Revoked User',
|
|
513
|
+
previousMemberStatus: 'revoked',
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(result.notified).toBe(true);
|
|
517
|
+
expect(emitSignalCalls.length).toBe(1);
|
|
518
|
+
|
|
519
|
+
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
520
|
+
expect(payload.previousMemberStatus).toBe('revoked');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('notifyGuardianOfAccessRequest persists canonical delivery rows from notification results', async () => {
|
|
524
|
+
mockEmitResult = {
|
|
525
|
+
signalId: 'sig-deliveries',
|
|
526
|
+
deduplicated: false,
|
|
527
|
+
dispatched: true,
|
|
528
|
+
reason: 'ok',
|
|
529
|
+
deliveryResults: [
|
|
530
|
+
{
|
|
531
|
+
channel: 'vellum',
|
|
532
|
+
destination: 'vellum',
|
|
533
|
+
status: 'sent',
|
|
534
|
+
conversationId: 'conv-guardian-access-request',
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
channel: 'telegram',
|
|
538
|
+
destination: 'guardian-chat-123',
|
|
539
|
+
status: 'sent',
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const result = notifyGuardianOfAccessRequest({
|
|
545
|
+
canonicalAssistantId: 'self',
|
|
546
|
+
sourceChannel: 'voice',
|
|
547
|
+
externalChatId: '+15556667777',
|
|
548
|
+
senderExternalUserId: '+15556667777',
|
|
549
|
+
senderName: 'Noah',
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
expect(result.notified).toBe(true);
|
|
553
|
+
if (!result.notified) return;
|
|
554
|
+
|
|
555
|
+
await flushAsyncAccessRequestBookkeeping();
|
|
556
|
+
|
|
557
|
+
const deliveries = listCanonicalGuardianDeliveries(result.requestId);
|
|
558
|
+
const vellum = deliveries.find((d) => d.destinationChannel === 'vellum');
|
|
559
|
+
const telegram = deliveries.find((d) => d.destinationChannel === 'telegram');
|
|
560
|
+
|
|
561
|
+
expect(vellum).toBeDefined();
|
|
562
|
+
expect(vellum!.destinationConversationId).toBe('conv-guardian-access-request');
|
|
563
|
+
expect(vellum!.status).toBe('sent');
|
|
564
|
+
expect(telegram).toBeDefined();
|
|
565
|
+
expect(telegram!.destinationChatId).toBe('guardian-chat-123');
|
|
566
|
+
expect(telegram!.status).toBe('sent');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test('notifyGuardianOfAccessRequest records failed vellum fallback when pipeline has no vellum delivery', async () => {
|
|
570
|
+
mockEmitResult = {
|
|
571
|
+
signalId: 'sig-no-vellum',
|
|
572
|
+
deduplicated: false,
|
|
573
|
+
dispatched: true,
|
|
574
|
+
reason: 'telegram-only',
|
|
575
|
+
deliveryResults: [
|
|
576
|
+
{
|
|
577
|
+
channel: 'telegram',
|
|
578
|
+
destination: 'guardian-chat-456',
|
|
579
|
+
status: 'sent',
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const result = notifyGuardianOfAccessRequest({
|
|
585
|
+
canonicalAssistantId: 'self',
|
|
586
|
+
sourceChannel: 'telegram',
|
|
587
|
+
externalChatId: 'chat-123',
|
|
588
|
+
senderExternalUserId: 'unknown-user',
|
|
589
|
+
senderName: 'Alice',
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
expect(result.notified).toBe(true);
|
|
593
|
+
if (!result.notified) return;
|
|
594
|
+
|
|
595
|
+
await flushAsyncAccessRequestBookkeeping();
|
|
596
|
+
|
|
597
|
+
const deliveries = listCanonicalGuardianDeliveries(result.requestId);
|
|
598
|
+
const vellum = deliveries.find((d) => d.destinationChannel === 'vellum');
|
|
599
|
+
const telegram = deliveries.find((d) => d.destinationChannel === 'telegram');
|
|
600
|
+
|
|
601
|
+
expect(vellum).toBeDefined();
|
|
602
|
+
expect(vellum!.status).toBe('failed');
|
|
603
|
+
expect(telegram).toBeDefined();
|
|
604
|
+
expect(telegram!.destinationChatId).toBe('guardian-chat-456');
|
|
605
|
+
expect(telegram!.status).toBe('sent');
|
|
606
|
+
});
|
|
457
607
|
});
|
|
@@ -96,22 +96,27 @@ describe('notification decision fallback copy', () => {
|
|
|
96
96
|
expect(decision.renderedCopy.vellum?.body).not.toContain('Action required: guardian.question');
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
test('enforces
|
|
99
|
+
test('enforces free-text answer instructions for guardian.question when requestCode exists', async () => {
|
|
100
100
|
const signal = makeSignal({
|
|
101
101
|
contextPayload: {
|
|
102
|
+
requestId: 'req-pending-1',
|
|
102
103
|
questionText: 'What is the gate code?',
|
|
103
104
|
requestCode: 'A1B2C3',
|
|
105
|
+
requestKind: 'pending_question',
|
|
106
|
+
callSessionId: 'call-1',
|
|
107
|
+
activeGuardianRequestCount: 1,
|
|
104
108
|
},
|
|
105
109
|
});
|
|
106
110
|
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
107
111
|
|
|
108
112
|
expect(decision.fallbackUsed).toBe(true);
|
|
109
113
|
expect(decision.renderedCopy.vellum?.body).toContain('A1B2C3');
|
|
110
|
-
expect(decision.renderedCopy.vellum?.body).toContain('
|
|
111
|
-
expect(decision.renderedCopy.vellum?.body).toContain('
|
|
114
|
+
expect(decision.renderedCopy.vellum?.body).toContain('<your answer>');
|
|
115
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain('approve');
|
|
116
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain('reject');
|
|
112
117
|
});
|
|
113
118
|
|
|
114
|
-
test('enforcement appends
|
|
119
|
+
test('enforcement appends free-text answer instructions when LLM copy only mentions request code', async () => {
|
|
115
120
|
configuredProvider = {
|
|
116
121
|
sendMessage: async () => ({ content: [] }),
|
|
117
122
|
};
|
|
@@ -134,8 +139,127 @@ describe('notification decision fallback copy', () => {
|
|
|
134
139
|
|
|
135
140
|
const signal = makeSignal({
|
|
136
141
|
contextPayload: {
|
|
142
|
+
requestId: 'req-pending-1',
|
|
137
143
|
questionText: 'What is the gate code?',
|
|
138
144
|
requestCode: 'A1B2C3',
|
|
145
|
+
requestKind: 'pending_question',
|
|
146
|
+
callSessionId: 'call-1',
|
|
147
|
+
activeGuardianRequestCount: 1,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
152
|
+
|
|
153
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
154
|
+
expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 <your answer>"');
|
|
155
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain('"A1B2C3 approve"');
|
|
156
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain('"A1B2C3 reject"');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('enforcement appends answer instructions when LLM copy incorrectly uses approve/reject wording', async () => {
|
|
160
|
+
configuredProvider = {
|
|
161
|
+
sendMessage: async () => ({ content: [] }),
|
|
162
|
+
};
|
|
163
|
+
extractedToolUse = {
|
|
164
|
+
name: 'record_notification_decision',
|
|
165
|
+
input: {
|
|
166
|
+
shouldNotify: true,
|
|
167
|
+
selectedChannels: ['vellum'],
|
|
168
|
+
reasoningSummary: 'LLM decision',
|
|
169
|
+
renderedCopy: {
|
|
170
|
+
vellum: {
|
|
171
|
+
title: 'Guardian Question',
|
|
172
|
+
body: 'Reference code: A1B2C3. Reply "A1B2C3 approve" or "A1B2C3 reject".',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
dedupeKey: 'guardian-question-wrong-instructions-test',
|
|
176
|
+
confidence: 0.9,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const signal = makeSignal({
|
|
181
|
+
contextPayload: {
|
|
182
|
+
requestId: 'req-pending-approve-phrasing',
|
|
183
|
+
questionText: 'What is the gate code?',
|
|
184
|
+
requestCode: 'A1B2C3',
|
|
185
|
+
requestKind: 'pending_question',
|
|
186
|
+
callSessionId: 'call-1',
|
|
187
|
+
activeGuardianRequestCount: 1,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
192
|
+
|
|
193
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
194
|
+
expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 <your answer>"');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('enforcement appends explicit approve/reject instructions for tool-approval guardian questions', async () => {
|
|
198
|
+
configuredProvider = {
|
|
199
|
+
sendMessage: async () => ({ content: [] }),
|
|
200
|
+
};
|
|
201
|
+
extractedToolUse = {
|
|
202
|
+
name: 'record_notification_decision',
|
|
203
|
+
input: {
|
|
204
|
+
shouldNotify: true,
|
|
205
|
+
selectedChannels: ['vellum'],
|
|
206
|
+
reasoningSummary: 'LLM decision',
|
|
207
|
+
renderedCopy: {
|
|
208
|
+
vellum: {
|
|
209
|
+
title: 'Guardian Question',
|
|
210
|
+
body: 'Use reference code A1B2C3 for this request.',
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
dedupeKey: 'guardian-question-tool-approval-test',
|
|
214
|
+
confidence: 0.9,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const signal = makeSignal({
|
|
219
|
+
contextPayload: {
|
|
220
|
+
requestId: 'req-grant-1',
|
|
221
|
+
questionText: 'Allow running host_bash?',
|
|
222
|
+
requestCode: 'A1B2C3',
|
|
223
|
+
requestKind: 'tool_grant_request',
|
|
224
|
+
toolName: 'host_bash',
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
229
|
+
|
|
230
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
231
|
+
expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 approve"');
|
|
232
|
+
expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 reject"');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('approval-mode enforcement removes conflicting answer-mode phrasing', async () => {
|
|
236
|
+
configuredProvider = {
|
|
237
|
+
sendMessage: async () => ({ content: [] }),
|
|
238
|
+
};
|
|
239
|
+
extractedToolUse = {
|
|
240
|
+
name: 'record_notification_decision',
|
|
241
|
+
input: {
|
|
242
|
+
shouldNotify: true,
|
|
243
|
+
selectedChannels: ['vellum'],
|
|
244
|
+
reasoningSummary: 'LLM decision',
|
|
245
|
+
renderedCopy: {
|
|
246
|
+
vellum: {
|
|
247
|
+
title: 'Guardian Question',
|
|
248
|
+
body: 'Reference code: A1B2C3. Reply "A1B2C3 <your answer>".',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
dedupeKey: 'guardian-question-approval-removes-answer-test',
|
|
252
|
+
confidence: 0.9,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const signal = makeSignal({
|
|
257
|
+
contextPayload: {
|
|
258
|
+
requestId: 'req-grant-2',
|
|
259
|
+
questionText: 'Allow running host_bash?',
|
|
260
|
+
requestCode: 'A1B2C3',
|
|
261
|
+
requestKind: 'tool_grant_request',
|
|
262
|
+
toolName: 'host_bash',
|
|
139
263
|
},
|
|
140
264
|
});
|
|
141
265
|
|
|
@@ -144,5 +268,6 @@ describe('notification decision fallback copy', () => {
|
|
|
144
268
|
expect(decision.fallbackUsed).toBe(false);
|
|
145
269
|
expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 approve"');
|
|
146
270
|
expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 reject"');
|
|
271
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain('<your answer>');
|
|
147
272
|
});
|
|
148
273
|
});
|