@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildGuardianCodeOnlyClarification,
|
|
5
|
+
buildGuardianDisambiguationExample,
|
|
6
|
+
buildGuardianDisambiguationLabel,
|
|
7
|
+
buildGuardianInvalidActionReply,
|
|
8
|
+
buildGuardianReplyDirective,
|
|
9
|
+
buildGuardianRequestCodeInstruction,
|
|
10
|
+
hasGuardianRequestCodeInstruction,
|
|
11
|
+
parseGuardianQuestionPayload,
|
|
12
|
+
resolveGuardianInstructionModeForRequest,
|
|
13
|
+
resolveGuardianInstructionModeFromFields,
|
|
14
|
+
resolveGuardianQuestionInstructionMode,
|
|
15
|
+
stripConflictingGuardianRequestInstructions,
|
|
16
|
+
} from '../notifications/guardian-question-mode.js';
|
|
17
|
+
|
|
18
|
+
describe('guardian-question-mode', () => {
|
|
19
|
+
test('parses pending_question payload as discriminated union', () => {
|
|
20
|
+
const parsed = parseGuardianQuestionPayload({
|
|
21
|
+
requestKind: 'pending_question',
|
|
22
|
+
requestId: 'req-1',
|
|
23
|
+
requestCode: 'A1B2C3',
|
|
24
|
+
questionText: 'What time works?',
|
|
25
|
+
callSessionId: 'call-1',
|
|
26
|
+
activeGuardianRequestCount: 2,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(parsed).not.toBeNull();
|
|
30
|
+
expect(parsed?.requestKind).toBe('pending_question');
|
|
31
|
+
if (!parsed || parsed.requestKind !== 'pending_question') return;
|
|
32
|
+
expect(parsed.callSessionId).toBe('call-1');
|
|
33
|
+
expect(parsed.activeGuardianRequestCount).toBe(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('parses tool_grant_request payload and requires toolName', () => {
|
|
37
|
+
const parsed = parseGuardianQuestionPayload({
|
|
38
|
+
requestKind: 'tool_grant_request',
|
|
39
|
+
requestId: 'req-2',
|
|
40
|
+
requestCode: 'D4E5F6',
|
|
41
|
+
questionText: 'Allow host bash?',
|
|
42
|
+
toolName: 'host_bash',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(parsed).not.toBeNull();
|
|
46
|
+
expect(parsed?.requestKind).toBe('tool_grant_request');
|
|
47
|
+
if (!parsed || parsed.requestKind !== 'tool_grant_request') return;
|
|
48
|
+
expect(parsed.toolName).toBe('host_bash');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('parses pending_question payload with optional toolName metadata', () => {
|
|
52
|
+
const parsed = parseGuardianQuestionPayload({
|
|
53
|
+
requestKind: 'pending_question',
|
|
54
|
+
requestId: 'req-voice-tool-1',
|
|
55
|
+
requestCode: 'AA11BB',
|
|
56
|
+
questionText: 'Allow send_email?',
|
|
57
|
+
callSessionId: 'call-voice-1',
|
|
58
|
+
activeGuardianRequestCount: 1,
|
|
59
|
+
toolName: 'send_email',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(parsed).not.toBeNull();
|
|
63
|
+
expect(parsed?.requestKind).toBe('pending_question');
|
|
64
|
+
if (!parsed || parsed.requestKind !== 'pending_question') return;
|
|
65
|
+
expect(parsed.toolName).toBe('send_email');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('rejects invalid pending_question payload missing required fields', () => {
|
|
69
|
+
const parsed = parseGuardianQuestionPayload({
|
|
70
|
+
requestKind: 'pending_question',
|
|
71
|
+
requestId: 'req-3',
|
|
72
|
+
requestCode: 'AAA111',
|
|
73
|
+
questionText: 'Missing call session and count',
|
|
74
|
+
});
|
|
75
|
+
expect(parsed).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('resolve mode uses discriminant for valid typed payloads', () => {
|
|
79
|
+
const resolved = resolveGuardianQuestionInstructionMode({
|
|
80
|
+
requestKind: 'pending_question',
|
|
81
|
+
requestId: 'req-1',
|
|
82
|
+
requestCode: 'A1B2C3',
|
|
83
|
+
questionText: 'What time works?',
|
|
84
|
+
callSessionId: 'call-1',
|
|
85
|
+
activeGuardianRequestCount: 2,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(resolved.mode).toBe('answer');
|
|
89
|
+
expect(resolved.requestKind).toBe('pending_question');
|
|
90
|
+
expect(resolved.legacyFallbackUsed).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('resolve mode uses legacy fallback when requestKind is missing', () => {
|
|
94
|
+
const resolved = resolveGuardianQuestionInstructionMode({
|
|
95
|
+
requestCode: 'A1B2C3',
|
|
96
|
+
questionText: 'Allow host bash?',
|
|
97
|
+
toolName: 'host_bash',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(resolved.mode).toBe('approval');
|
|
101
|
+
expect(resolved.requestKind).toBeNull();
|
|
102
|
+
expect(resolved.legacyFallbackUsed).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('resolve mode treats pending_question with toolName as approval-mode', () => {
|
|
106
|
+
const resolved = resolveGuardianQuestionInstructionMode({
|
|
107
|
+
requestKind: 'pending_question',
|
|
108
|
+
requestId: 'req-voice-tool-2',
|
|
109
|
+
requestCode: 'CC22DD',
|
|
110
|
+
questionText: 'Allow send_email?',
|
|
111
|
+
callSessionId: 'call-voice-2',
|
|
112
|
+
activeGuardianRequestCount: 1,
|
|
113
|
+
toolName: 'send_email',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(resolved.mode).toBe('approval');
|
|
117
|
+
expect(resolved.requestKind).toBe('pending_question');
|
|
118
|
+
expect(resolved.legacyFallbackUsed).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('resolveGuardianInstructionModeFromFields returns null for unknown request kind', () => {
|
|
122
|
+
const resolved = resolveGuardianInstructionModeFromFields('unknown_kind', 'send_email');
|
|
123
|
+
expect(resolved).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('answer-mode instruction detection rejects approval phrasing', () => {
|
|
127
|
+
const code = 'A1B2C3';
|
|
128
|
+
const wrongInstruction = buildGuardianRequestCodeInstruction(code, 'approval');
|
|
129
|
+
const correctInstruction = buildGuardianRequestCodeInstruction(code, 'answer');
|
|
130
|
+
|
|
131
|
+
expect(hasGuardianRequestCodeInstruction(wrongInstruction, code, 'answer')).toBe(false);
|
|
132
|
+
expect(hasGuardianRequestCodeInstruction(correctInstruction, code, 'answer')).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('buildGuardianReplyDirective uses mode-specific wording', () => {
|
|
136
|
+
expect(buildGuardianReplyDirective('A1B2C3', 'approval')).toBe('Reply "A1B2C3 approve" or "A1B2C3 reject".');
|
|
137
|
+
expect(buildGuardianReplyDirective('A1B2C3', 'answer')).toBe('Reply "A1B2C3 <your answer>".');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('resolveGuardianInstructionModeForRequest handles tool-backed pending_question as approval', () => {
|
|
141
|
+
expect(
|
|
142
|
+
resolveGuardianInstructionModeForRequest({
|
|
143
|
+
kind: 'pending_question',
|
|
144
|
+
toolName: 'send_email',
|
|
145
|
+
}),
|
|
146
|
+
).toBe('approval');
|
|
147
|
+
expect(
|
|
148
|
+
resolveGuardianInstructionModeForRequest({
|
|
149
|
+
kind: 'pending_question',
|
|
150
|
+
toolName: null,
|
|
151
|
+
}),
|
|
152
|
+
).toBe('answer');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('centralized guardian response copy builders produce mode-specific copy', () => {
|
|
156
|
+
expect(buildGuardianInvalidActionReply('approval', 'A1B2C3')).toContain('approve');
|
|
157
|
+
expect(buildGuardianInvalidActionReply('answer', 'A1B2C3')).toContain('<your answer>');
|
|
158
|
+
|
|
159
|
+
expect(
|
|
160
|
+
buildGuardianCodeOnlyClarification('approval', {
|
|
161
|
+
requestCode: 'A1B2C3',
|
|
162
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
163
|
+
toolName: 'send_email',
|
|
164
|
+
}),
|
|
165
|
+
).toContain('I found request A1B2C3 for send_email.');
|
|
166
|
+
expect(
|
|
167
|
+
buildGuardianCodeOnlyClarification('answer', {
|
|
168
|
+
requestCode: 'A1B2C3',
|
|
169
|
+
questionText: 'What time works best?',
|
|
170
|
+
}),
|
|
171
|
+
).toContain('I found question A1B2C3.');
|
|
172
|
+
|
|
173
|
+
expect(
|
|
174
|
+
buildGuardianDisambiguationLabel('approval', {
|
|
175
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
176
|
+
toolName: 'send_email',
|
|
177
|
+
}),
|
|
178
|
+
).toBe('send_email');
|
|
179
|
+
expect(
|
|
180
|
+
buildGuardianDisambiguationLabel('answer', {
|
|
181
|
+
questionText: 'What time works best?',
|
|
182
|
+
}),
|
|
183
|
+
).toBe('What time works best?');
|
|
184
|
+
|
|
185
|
+
expect(buildGuardianDisambiguationExample('approval', 'A1B2C3')).toBe(
|
|
186
|
+
'For approvals: reply "A1B2C3 approve" or "A1B2C3 reject".',
|
|
187
|
+
);
|
|
188
|
+
expect(buildGuardianDisambiguationExample('answer', 'A1B2C3')).toBe(
|
|
189
|
+
'For questions: reply "A1B2C3 <your answer>".',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('stripConflictingGuardianRequestInstructions removes opposite-mode instructions', () => {
|
|
194
|
+
const approvalText = 'Reference code: A1B2C3. Reply "A1B2C3 approve" or "A1B2C3 reject".';
|
|
195
|
+
const answerText = 'Reference code: A1B2C3. Reply "A1B2C3 <your answer>".';
|
|
196
|
+
|
|
197
|
+
expect(stripConflictingGuardianRequestInstructions(approvalText, 'A1B2C3', 'answer')).toBe('');
|
|
198
|
+
expect(stripConflictingGuardianRequestInstructions(answerText, 'A1B2C3', 'approval')).toBe('');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -475,6 +475,69 @@ describe('routing invariant: code-only messages return clarification', () => {
|
|
|
475
475
|
expect(unchanged!.status).toBe('pending');
|
|
476
476
|
});
|
|
477
477
|
|
|
478
|
+
test('code-only pending_question asks for free-text answer (not approve/reject)', async () => {
|
|
479
|
+
const req = createCanonicalGuardianRequest({
|
|
480
|
+
kind: 'pending_question',
|
|
481
|
+
sourceType: 'voice',
|
|
482
|
+
sourceChannel: 'voice',
|
|
483
|
+
conversationId: 'conv-1',
|
|
484
|
+
guardianExternalUserId: 'guardian-1',
|
|
485
|
+
callSessionId: 'call-1',
|
|
486
|
+
pendingQuestionId: 'pq-1',
|
|
487
|
+
requestCode: 'A2B3C4',
|
|
488
|
+
questionText: 'What time works best?',
|
|
489
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const result = await routeGuardianReply(replyCtx({
|
|
493
|
+
messageText: 'A2B3C4',
|
|
494
|
+
conversationId: 'conv-1',
|
|
495
|
+
}));
|
|
496
|
+
|
|
497
|
+
expect(result.consumed).toBe(true);
|
|
498
|
+
expect(result.type).toBe('code_only_clarification');
|
|
499
|
+
expect(result.decisionApplied).toBe(false);
|
|
500
|
+
expect(result.replyText).toContain('A2B3C4');
|
|
501
|
+
expect(result.replyText).toContain('<your answer>');
|
|
502
|
+
expect(result.replyText).not.toContain('approve');
|
|
503
|
+
expect(result.replyText).not.toContain('reject');
|
|
504
|
+
|
|
505
|
+
const unchanged = getCanonicalGuardianRequest(req.id);
|
|
506
|
+
expect(unchanged!.status).toBe('pending');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('code-only tool-backed pending_question asks for approve/reject decision', async () => {
|
|
510
|
+
const req = createCanonicalGuardianRequest({
|
|
511
|
+
kind: 'pending_question',
|
|
512
|
+
sourceType: 'voice',
|
|
513
|
+
sourceChannel: 'voice',
|
|
514
|
+
conversationId: 'conv-1',
|
|
515
|
+
guardianExternalUserId: 'guardian-1',
|
|
516
|
+
callSessionId: 'call-2',
|
|
517
|
+
pendingQuestionId: 'pq-2',
|
|
518
|
+
requestCode: 'B2C3D4',
|
|
519
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
520
|
+
toolName: 'send_email',
|
|
521
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const result = await routeGuardianReply(replyCtx({
|
|
525
|
+
messageText: 'B2C3D4',
|
|
526
|
+
conversationId: 'conv-1',
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
expect(result.consumed).toBe(true);
|
|
530
|
+
expect(result.type).toBe('code_only_clarification');
|
|
531
|
+
expect(result.decisionApplied).toBe(false);
|
|
532
|
+
expect(result.replyText).toContain('B2C3D4');
|
|
533
|
+
expect(result.replyText).toContain('approve');
|
|
534
|
+
expect(result.replyText).toContain('reject');
|
|
535
|
+
expect(result.replyText).not.toContain('<your answer>');
|
|
536
|
+
|
|
537
|
+
const unchanged = getCanonicalGuardianRequest(req.id);
|
|
538
|
+
expect(unchanged!.status).toBe('pending');
|
|
539
|
+
});
|
|
540
|
+
|
|
478
541
|
test('code with decision text does apply the decision', async () => {
|
|
479
542
|
const req = createCanonicalGuardianRequest({
|
|
480
543
|
kind: 'tool_approval',
|
|
@@ -701,6 +764,51 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
701
764
|
expect(result.replyText).toContain('BBB222');
|
|
702
765
|
});
|
|
703
766
|
|
|
767
|
+
test('disambiguation treats tool-backed pending_question as approval request', async () => {
|
|
768
|
+
const answerRequest = createCanonicalGuardianRequest({
|
|
769
|
+
kind: 'pending_question',
|
|
770
|
+
sourceType: 'voice',
|
|
771
|
+
sourceChannel: 'voice',
|
|
772
|
+
conversationId: 'conv-1',
|
|
773
|
+
guardianExternalUserId: 'guardian-1',
|
|
774
|
+
callSessionId: 'call-answer',
|
|
775
|
+
pendingQuestionId: 'pq-answer',
|
|
776
|
+
requestCode: 'ABC123',
|
|
777
|
+
questionText: 'What time works best?',
|
|
778
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const approvalRequest = createCanonicalGuardianRequest({
|
|
782
|
+
kind: 'pending_question',
|
|
783
|
+
sourceType: 'voice',
|
|
784
|
+
sourceChannel: 'voice',
|
|
785
|
+
conversationId: 'conv-1',
|
|
786
|
+
guardianExternalUserId: 'guardian-1',
|
|
787
|
+
callSessionId: 'call-approval',
|
|
788
|
+
pendingQuestionId: 'pq-approval',
|
|
789
|
+
requestCode: 'DEF456',
|
|
790
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
791
|
+
toolName: 'send_email',
|
|
792
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const result = await routeGuardianReply(replyCtx({
|
|
796
|
+
messageText: 'approve',
|
|
797
|
+
conversationId: 'conv-guardian-thread',
|
|
798
|
+
pendingRequestIds: [answerRequest.id, approvalRequest.id],
|
|
799
|
+
approvalConversationGenerator: undefined,
|
|
800
|
+
}));
|
|
801
|
+
|
|
802
|
+
expect(result.consumed).toBe(true);
|
|
803
|
+
expect(result.type).toBe('disambiguation_needed');
|
|
804
|
+
expect(result.decisionApplied).toBe(false);
|
|
805
|
+
expect(result.replyText).toContain('ABC123');
|
|
806
|
+
expect(result.replyText).toContain('DEF456');
|
|
807
|
+
expect(result.replyText).toContain('send_email');
|
|
808
|
+
expect(result.replyText).toContain('For questions: reply "ABC123 <your answer>".');
|
|
809
|
+
expect(result.replyText).toContain('For approvals: reply "DEF456 approve" or "DEF456 reject".');
|
|
810
|
+
});
|
|
811
|
+
|
|
704
812
|
test('single pending request does not need disambiguation', async () => {
|
|
705
813
|
const req = createCanonicalGuardianRequest({
|
|
706
814
|
kind: 'tool_approval',
|
|
@@ -1130,4 +1238,74 @@ describe('routing invariant: invite handoff bypass for access requests', () => {
|
|
|
1130
1238
|
const resolved = getCanonicalGuardianRequest(req.id);
|
|
1131
1239
|
expect(resolved!.status).toBe('approved');
|
|
1132
1240
|
});
|
|
1241
|
+
|
|
1242
|
+
test('trusted desktop access-request approval returns a verification code reply', async () => {
|
|
1243
|
+
const req = createCanonicalGuardianRequest({
|
|
1244
|
+
kind: 'access_request',
|
|
1245
|
+
sourceType: 'channel',
|
|
1246
|
+
sourceChannel: 'telegram',
|
|
1247
|
+
conversationId: 'conv-access-desktop',
|
|
1248
|
+
guardianExternalUserId: 'guardian-1',
|
|
1249
|
+
requestCode: 'C0D3A5',
|
|
1250
|
+
toolName: 'ingress_access_request',
|
|
1251
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
const result = await routeGuardianReply({
|
|
1255
|
+
messageText: 'C0D3A5 approve',
|
|
1256
|
+
channel: 'vellum',
|
|
1257
|
+
actor: trustedActor({ channel: 'vellum' }),
|
|
1258
|
+
conversationId: 'conv-guardian-thread',
|
|
1259
|
+
pendingRequestIds: [req.id],
|
|
1260
|
+
approvalConversationGenerator: undefined,
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
expect(result.consumed).toBe(true);
|
|
1264
|
+
expect(result.decisionApplied).toBe(true);
|
|
1265
|
+
expect(result.replyText).toContain('verification code');
|
|
1266
|
+
expect(result.replyText).toMatch(/\b\d{6}\b/);
|
|
1267
|
+
|
|
1268
|
+
const resolved = getCanonicalGuardianRequest(req.id);
|
|
1269
|
+
expect(resolved!.status).toBe('approved');
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
test('NL decision path preserves resolver verification code reply text', async () => {
|
|
1273
|
+
const req = createCanonicalGuardianRequest({
|
|
1274
|
+
kind: 'access_request',
|
|
1275
|
+
sourceType: 'channel',
|
|
1276
|
+
sourceChannel: 'telegram',
|
|
1277
|
+
conversationId: 'conv-access-desktop-nl',
|
|
1278
|
+
guardianExternalUserId: 'guardian-1',
|
|
1279
|
+
requesterExternalUserId: 'requester-1',
|
|
1280
|
+
requesterChatId: 'chat-1',
|
|
1281
|
+
requestCode: 'A1B2C3',
|
|
1282
|
+
toolName: 'ingress_access_request',
|
|
1283
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
const approvalConversationGenerator = async () => ({
|
|
1287
|
+
disposition: 'approve_once' as const,
|
|
1288
|
+
replyText: 'Access approved.',
|
|
1289
|
+
targetRequestId: req.id,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
const result = await routeGuardianReply({
|
|
1293
|
+
messageText: 'please approve this request',
|
|
1294
|
+
channel: 'vellum',
|
|
1295
|
+
actor: trustedActor({ channel: 'vellum' }),
|
|
1296
|
+
conversationId: 'conv-guardian-thread',
|
|
1297
|
+
pendingRequestIds: [req.id],
|
|
1298
|
+
approvalConversationGenerator: approvalConversationGenerator as any,
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
expect(result.consumed).toBe(true);
|
|
1302
|
+
expect(result.decisionApplied).toBe(true);
|
|
1303
|
+
expect(result.type).toBe('canonical_decision_applied');
|
|
1304
|
+
expect(result.replyText).toContain('verification code');
|
|
1305
|
+
expect(result.replyText).toMatch(/\b\d{6}\b/);
|
|
1306
|
+
expect(result.replyText).not.toBe('Access approved.');
|
|
1307
|
+
|
|
1308
|
+
const resolved = getCanonicalGuardianRequest(req.id);
|
|
1309
|
+
expect(resolved!.status).toBe('approved');
|
|
1310
|
+
});
|
|
1133
1311
|
});
|