@vellumai/assistant 0.4.3 → 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 +40 -3
- package/README.md +43 -35
- 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__/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 +125 -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__/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 -87
- 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 +4 -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__/guardian-actions-endpoint.test.ts +19 -14
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -4
- 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__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +131 -8
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +62 -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 +841 -39
- 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 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -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__/twilio-config.test.ts +2 -13
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +10 -2
- package/src/approvals/guardian-request-resolvers.ts +128 -9
- package/src/calls/call-constants.ts +21 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-domain.ts +28 -7
- 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 +424 -12
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +1 -1
- package/src/calls/types.ts +3 -1
- 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 +146 -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 +1 -0
- package/src/config/calls-schema.ts +24 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -0
- 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 +10 -9
- 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 +5 -55
- package/src/daemon/handlers/config-inbox.ts +9 -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/pairing.ts +2 -0
- package/src/daemon/handlers/sessions.ts +48 -3
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/integrations.ts +1 -99
- 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 +14 -1
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +22 -11
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +3 -0
- package/src/daemon/session-surfaces.ts +3 -2
- 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/db-init.ts +4 -0
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +16 -0
- 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 +39 -1
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +16 -8
- 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 +71 -1
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -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 +0 -3
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +65 -12
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/invite-redemption-service.ts +8 -0
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/conversation-routes.ts +140 -52
- package/src/runtime/routes/events-routes.ts +20 -5
- 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-message-handler.ts +143 -2
- 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/permission-checker.ts +15 -4
- package/src/tools/tool-approval-handler.ts +242 -18
- 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
|
});
|
|
@@ -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,
|
|
@@ -58,16 +58,23 @@ mock.module('../config/env.js', () => ({
|
|
|
58
58
|
|
|
59
59
|
// Track emitNotificationSignal calls
|
|
60
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
|
+
};
|
|
61
74
|
mock.module('../notifications/emit-signal.js', () => ({
|
|
62
75
|
emitNotificationSignal: async (params: Record<string, unknown>) => {
|
|
63
76
|
emitSignalCalls.push(params);
|
|
64
|
-
return
|
|
65
|
-
signalId: 'mock-signal-id',
|
|
66
|
-
deduplicated: false,
|
|
67
|
-
dispatched: true,
|
|
68
|
-
reason: 'mock',
|
|
69
|
-
deliveryResults: [],
|
|
70
|
-
};
|
|
77
|
+
return mockEmitResult;
|
|
71
78
|
},
|
|
72
79
|
}));
|
|
73
80
|
|
|
@@ -79,7 +86,10 @@ mock.module('../runtime/gateway-client.js', () => ({
|
|
|
79
86
|
},
|
|
80
87
|
}));
|
|
81
88
|
|
|
82
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
listCanonicalGuardianDeliveries,
|
|
91
|
+
listCanonicalGuardianRequests,
|
|
92
|
+
} from '../memory/canonical-guardian-store.js';
|
|
83
93
|
import {
|
|
84
94
|
createBinding,
|
|
85
95
|
} from '../memory/channel-guardian-store.js';
|
|
@@ -111,6 +121,17 @@ function resetState(): void {
|
|
|
111
121
|
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
112
122
|
emitSignalCalls.length = 0;
|
|
113
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));
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
@@ -481,4 +502,106 @@ describe('access-request-helper unit tests', () => {
|
|
|
481
502
|
expect(typeof payload.requestCode).toBe('string');
|
|
482
503
|
expect((payload.requestCode as string).length).toBe(6);
|
|
483
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
|
+
});
|
|
484
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
|
});
|
|
@@ -55,21 +55,67 @@ describe('notification decision strategy', () => {
|
|
|
55
55
|
expect(copy.vellum!.body).toContain('What is the gate code?');
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test('guardian.question template includes
|
|
58
|
+
test('guardian.question template includes free-text answer instructions when requestCode is present', () => {
|
|
59
59
|
const signal = makeSignal({
|
|
60
60
|
sourceEventName: 'guardian.question',
|
|
61
61
|
contextPayload: {
|
|
62
|
+
requestId: 'req-pending-1',
|
|
62
63
|
questionText: 'What is the gate code?',
|
|
63
64
|
requestCode: 'A1B2C3',
|
|
65
|
+
requestKind: 'pending_question',
|
|
66
|
+
callSessionId: 'call-1',
|
|
67
|
+
activeGuardianRequestCount: 1,
|
|
64
68
|
},
|
|
65
69
|
});
|
|
66
70
|
|
|
67
71
|
const copy = composeFallbackCopy(signal, channels);
|
|
68
72
|
expect(copy.vellum).toBeDefined();
|
|
69
73
|
expect(copy.vellum!.body).toContain('A1B2C3');
|
|
74
|
+
expect(copy.vellum!.body).toContain('<your answer>');
|
|
75
|
+
expect(copy.vellum!.body).not.toContain('approve');
|
|
76
|
+
expect(copy.vellum!.body).not.toContain('reject');
|
|
77
|
+
expect(copy.telegram!.deliveryText).toContain('A1B2C3');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('guardian.question template uses approve/reject instructions for approval-kind request', () => {
|
|
81
|
+
const signal = makeSignal({
|
|
82
|
+
sourceEventName: 'guardian.question',
|
|
83
|
+
contextPayload: {
|
|
84
|
+
requestId: 'req-grant-1',
|
|
85
|
+
questionText: 'Allow running host_bash?',
|
|
86
|
+
requestCode: 'D4E5F6',
|
|
87
|
+
requestKind: 'tool_grant_request',
|
|
88
|
+
toolName: 'host_bash',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const copy = composeFallbackCopy(signal, channels);
|
|
93
|
+
expect(copy.vellum).toBeDefined();
|
|
94
|
+
expect(copy.vellum!.body).toContain('D4E5F6');
|
|
70
95
|
expect(copy.vellum!.body).toContain('approve');
|
|
71
96
|
expect(copy.vellum!.body).toContain('reject');
|
|
72
|
-
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('guardian.question template uses approve/reject for tool-backed pending_question payloads', () => {
|
|
100
|
+
const signal = makeSignal({
|
|
101
|
+
sourceEventName: 'guardian.question',
|
|
102
|
+
contextPayload: {
|
|
103
|
+
requestId: 'req-voice-tool-1',
|
|
104
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
105
|
+
requestCode: 'A1B2C3',
|
|
106
|
+
requestKind: 'pending_question',
|
|
107
|
+
callSessionId: 'call-1',
|
|
108
|
+
activeGuardianRequestCount: 1,
|
|
109
|
+
toolName: 'send_email',
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const copy = composeFallbackCopy(signal, channels);
|
|
114
|
+
expect(copy.vellum).toBeDefined();
|
|
115
|
+
expect(copy.vellum!.body).toContain('A1B2C3');
|
|
116
|
+
expect(copy.vellum!.body).toContain('approve');
|
|
117
|
+
expect(copy.vellum!.body).toContain('reject');
|
|
118
|
+
expect(copy.vellum!.body).not.toContain('<your answer>');
|
|
73
119
|
});
|
|
74
120
|
|
|
75
121
|
test('reminder.fired template uses message from payload', () => {
|
|
@@ -196,6 +242,20 @@ describe('notification decision strategy', () => {
|
|
|
196
242
|
expect(copy.vellum!.body).toContain('open invite flow');
|
|
197
243
|
});
|
|
198
244
|
|
|
245
|
+
test('ingress.access_request template includes revoked-member context when provided', () => {
|
|
246
|
+
const signal = makeSignal({
|
|
247
|
+
sourceEventName: 'ingress.access_request',
|
|
248
|
+
contextPayload: {
|
|
249
|
+
senderIdentifier: 'Charlie',
|
|
250
|
+
previousMemberStatus: 'revoked',
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const copy = composeFallbackCopy(signal, channels);
|
|
255
|
+
expect(copy.vellum).toBeDefined();
|
|
256
|
+
expect(copy.vellum!.body).toContain('previously revoked');
|
|
257
|
+
});
|
|
258
|
+
|
|
199
259
|
test('ingress.access_request template includes caller name for voice-originated requests', () => {
|
|
200
260
|
// In production, senderIdentifier resolves to senderName for voice
|
|
201
261
|
// calls (senderName || senderUsername || senderExternalUserId), so
|
|
@@ -181,6 +181,9 @@ describe('ASK_GUARDIAN canonical notification path', () => {
|
|
|
181
181
|
const payload = signalParams.contextPayload as Record<string, unknown>;
|
|
182
182
|
expect(payload.questionText).toBe('What is the gate code?');
|
|
183
183
|
expect(payload.callSessionId).toBe(session.id);
|
|
184
|
+
expect(payload.requestKind).toBe('pending_question');
|
|
185
|
+
expect(payload.toolName).toBeUndefined();
|
|
186
|
+
expect(payload.pendingQuestionId).toBeUndefined();
|
|
184
187
|
expect(payload.requestId).toBeDefined();
|
|
185
188
|
expect(payload.requestCode).toBeDefined();
|
|
186
189
|
});
|
|
@@ -391,6 +391,7 @@ function createCtx(overrides?: Partial<HandlerContext>): {
|
|
|
391
391
|
setChannelCapabilities: noop,
|
|
392
392
|
setGuardianContext: noop,
|
|
393
393
|
setCommandIntent: noop,
|
|
394
|
+
updateClient: noop,
|
|
394
395
|
processMessage: async () => {},
|
|
395
396
|
getQueueDepth: () => 0,
|
|
396
397
|
setPreactivatedSkillIds: noop,
|