@vellumai/assistant 0.4.3 → 0.4.5
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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard test: prevent reintroduction of hard-coded pointer copy in
|
|
3
|
+
* relay-server.ts, call-controller.ts, and call-domain.ts.
|
|
4
|
+
*
|
|
5
|
+
* Deterministic fallback literals should only exist in the pointer
|
|
6
|
+
* composer file (call-pointer-message-composer.ts). The call-site
|
|
7
|
+
* files should route through addPointerMessage() exclusively.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from 'bun:test';
|
|
13
|
+
|
|
14
|
+
const srcDir = join(import.meta.dir, '..');
|
|
15
|
+
|
|
16
|
+
// These files must NOT contain inline pointer copy strings.
|
|
17
|
+
const guardedFiles = [
|
|
18
|
+
'calls/relay-server.ts',
|
|
19
|
+
'calls/call-controller.ts',
|
|
20
|
+
'calls/call-domain.ts',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Patterns that indicate inline pointer copy rather than routing through
|
|
24
|
+
// addPointerMessage. We check for the distinctive emoji + "Call to" prefix
|
|
25
|
+
// that the old hard-coded templates used.
|
|
26
|
+
const forbiddenPatterns = [
|
|
27
|
+
/["\u{1F4DE}].*Call to.*(?:started|completed|failed)/u,
|
|
28
|
+
/["\u{2705}].*Guardian verification.*succeeded/u,
|
|
29
|
+
/["\u{274C}].*Guardian verification.*failed/u,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
describe('no hardcoded pointer copy in call-site files', () => {
|
|
33
|
+
for (const file of guardedFiles) {
|
|
34
|
+
test(`${file} does not contain inline pointer copy`, () => {
|
|
35
|
+
const content = readFileSync(join(srcDir, file), 'utf-8');
|
|
36
|
+
for (const pattern of forbiddenPatterns) {
|
|
37
|
+
const match = pattern.exec(content);
|
|
38
|
+
expect(match).toBeNull();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the callback handoff notification fallback copy template.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the `ingress.access_request.callback_handoff` template in
|
|
5
|
+
* copy-composer.ts renders caller identity, request code, and trusted-contact
|
|
6
|
+
* member reference correctly — including graceful fallback when fields are missing.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
|
|
10
|
+
import { composeFallbackCopy } from '../notifications/copy-composer.js';
|
|
11
|
+
import type { NotificationSignal } from '../notifications/signal.js';
|
|
12
|
+
|
|
13
|
+
function buildSignal(payloadOverrides: Record<string, unknown> = {}): NotificationSignal {
|
|
14
|
+
return {
|
|
15
|
+
signalId: 'test-signal-1',
|
|
16
|
+
assistantId: 'self',
|
|
17
|
+
createdAt: Date.now(),
|
|
18
|
+
sourceChannel: 'voice',
|
|
19
|
+
sourceSessionId: 'test-session-1',
|
|
20
|
+
sourceEventName: 'ingress.access_request.callback_handoff',
|
|
21
|
+
contextPayload: {
|
|
22
|
+
requestId: 'req-123',
|
|
23
|
+
requestCode: null,
|
|
24
|
+
callSessionId: 'call-456',
|
|
25
|
+
sourceChannel: 'voice',
|
|
26
|
+
reason: 'timeout',
|
|
27
|
+
callbackOptIn: true,
|
|
28
|
+
callerPhoneNumber: '+15551234567',
|
|
29
|
+
callerName: null,
|
|
30
|
+
requesterExternalUserId: '+15551234567',
|
|
31
|
+
requesterChatId: '+15551234567',
|
|
32
|
+
requesterMemberId: null,
|
|
33
|
+
requesterMemberSourceChannel: null,
|
|
34
|
+
...payloadOverrides,
|
|
35
|
+
},
|
|
36
|
+
attentionHints: {
|
|
37
|
+
requiresAction: false,
|
|
38
|
+
urgency: 'medium',
|
|
39
|
+
isAsyncBackground: true,
|
|
40
|
+
visibleInSourceNow: false,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('callback handoff copy template', () => {
|
|
46
|
+
test('renders caller name and phone when both are present', () => {
|
|
47
|
+
const signal = buildSignal({
|
|
48
|
+
callerName: 'Alice',
|
|
49
|
+
callerPhoneNumber: '+15551234567',
|
|
50
|
+
});
|
|
51
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
52
|
+
const copy = result.vellum!;
|
|
53
|
+
|
|
54
|
+
expect(copy.title).toBe('Callback Requested');
|
|
55
|
+
expect(copy.body).toContain('Alice (+15551234567)');
|
|
56
|
+
expect(copy.body).toContain('callback');
|
|
57
|
+
expect(copy.body).toContain('unreachable');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('renders phone number only when caller name is missing', () => {
|
|
61
|
+
const signal = buildSignal({
|
|
62
|
+
callerName: null,
|
|
63
|
+
callerPhoneNumber: '+15559876543',
|
|
64
|
+
});
|
|
65
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
66
|
+
const copy = result.vellum!;
|
|
67
|
+
|
|
68
|
+
expect(copy.body).toContain('+15559876543');
|
|
69
|
+
expect(copy.body).not.toContain('null');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('renders caller name only when phone is missing', () => {
|
|
73
|
+
const signal = buildSignal({
|
|
74
|
+
callerName: 'Bob',
|
|
75
|
+
callerPhoneNumber: null,
|
|
76
|
+
});
|
|
77
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
78
|
+
const copy = result.vellum!;
|
|
79
|
+
|
|
80
|
+
expect(copy.body).toContain('Bob');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('falls back to "An unknown caller" when both name and phone are missing', () => {
|
|
84
|
+
const signal = buildSignal({
|
|
85
|
+
callerName: null,
|
|
86
|
+
callerPhoneNumber: null,
|
|
87
|
+
});
|
|
88
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
89
|
+
const copy = result.vellum!;
|
|
90
|
+
|
|
91
|
+
expect(copy.body).toContain('An unknown caller');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('includes request code when present', () => {
|
|
95
|
+
const signal = buildSignal({
|
|
96
|
+
callerName: 'Charlie',
|
|
97
|
+
callerPhoneNumber: '+15551111111',
|
|
98
|
+
requestCode: 'a1b2c3',
|
|
99
|
+
});
|
|
100
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
101
|
+
const copy = result.vellum!;
|
|
102
|
+
|
|
103
|
+
expect(copy.body).toContain('A1B2C3');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('omits request code line when not present', () => {
|
|
107
|
+
const signal = buildSignal({
|
|
108
|
+
callerName: 'Charlie',
|
|
109
|
+
callerPhoneNumber: '+15551111111',
|
|
110
|
+
requestCode: null,
|
|
111
|
+
});
|
|
112
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
113
|
+
const copy = result.vellum!;
|
|
114
|
+
|
|
115
|
+
expect(copy.body).not.toContain('Request code');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('includes trusted-contact member reference when requesterMemberId is present', () => {
|
|
119
|
+
const signal = buildSignal({
|
|
120
|
+
callerName: 'Diana',
|
|
121
|
+
callerPhoneNumber: '+15552222222',
|
|
122
|
+
requesterMemberId: 'member-789',
|
|
123
|
+
});
|
|
124
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
125
|
+
const copy = result.vellum!;
|
|
126
|
+
|
|
127
|
+
expect(copy.body).toContain('trusted contact');
|
|
128
|
+
expect(copy.body).toContain('member-789');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('omits member reference line when requesterMemberId is null', () => {
|
|
132
|
+
const signal = buildSignal({
|
|
133
|
+
callerName: 'Eve',
|
|
134
|
+
callerPhoneNumber: '+15553333333',
|
|
135
|
+
requesterMemberId: null,
|
|
136
|
+
});
|
|
137
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
138
|
+
const copy = result.vellum!;
|
|
139
|
+
|
|
140
|
+
expect(copy.body).not.toContain('trusted contact');
|
|
141
|
+
expect(copy.body).not.toContain('member');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('telegram channel gets deliveryText fallback', () => {
|
|
145
|
+
const signal = buildSignal({
|
|
146
|
+
callerName: 'Frank',
|
|
147
|
+
callerPhoneNumber: '+15554444444',
|
|
148
|
+
});
|
|
149
|
+
const result = composeFallbackCopy(signal, ['telegram']);
|
|
150
|
+
const copy = result.telegram!;
|
|
151
|
+
|
|
152
|
+
expect(copy.deliveryText).toBeDefined();
|
|
153
|
+
expect(copy.deliveryText!.length).toBeGreaterThan(0);
|
|
154
|
+
expect(copy.deliveryText).toContain('Frank');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('sms channel gets deliveryText fallback', () => {
|
|
158
|
+
const signal = buildSignal({
|
|
159
|
+
callerName: 'Grace',
|
|
160
|
+
callerPhoneNumber: '+15555555555',
|
|
161
|
+
});
|
|
162
|
+
const result = composeFallbackCopy(signal, ['sms']);
|
|
163
|
+
const copy = result.sms!;
|
|
164
|
+
|
|
165
|
+
expect(copy.deliveryText).toBeDefined();
|
|
166
|
+
expect(copy.deliveryText!.length).toBeGreaterThan(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('full payload renders all fields correctly', () => {
|
|
170
|
+
const signal = buildSignal({
|
|
171
|
+
callerName: 'Hank',
|
|
172
|
+
callerPhoneNumber: '+15556666666',
|
|
173
|
+
requestCode: 'ff00aa',
|
|
174
|
+
requesterMemberId: 'member-full-test',
|
|
175
|
+
});
|
|
176
|
+
const result = composeFallbackCopy(signal, ['vellum']);
|
|
177
|
+
const copy = result.vellum!;
|
|
178
|
+
|
|
179
|
+
expect(copy.title).toBe('Callback Requested');
|
|
180
|
+
expect(copy.body).toContain('Hank (+15556666666)');
|
|
181
|
+
expect(copy.body).toContain('FF00AA');
|
|
182
|
+
expect(copy.body).toContain('member-full-test');
|
|
183
|
+
expect(copy.body).toContain('callback');
|
|
184
|
+
expect(copy.body).toContain('unreachable');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -1319,11 +1319,12 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1319
1319
|
deliverSpy.mockRestore();
|
|
1320
1320
|
});
|
|
1321
1321
|
|
|
1322
|
-
test('verification code with explicit assistantId resolves against
|
|
1322
|
+
test('verification code with explicit assistantId resolves against canonical scope', async () => {
|
|
1323
1323
|
const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
|
|
1324
1324
|
const { getGuardianBinding } = await import('../runtime/channel-guardian-service.js');
|
|
1325
1325
|
|
|
1326
|
-
|
|
1326
|
+
// All assistant IDs canonicalize to 'self' in the single-tenant daemon
|
|
1327
|
+
const { secret } = createVerificationChallenge('self', 'telegram');
|
|
1327
1328
|
|
|
1328
1329
|
const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
|
|
1329
1330
|
|
|
@@ -1338,17 +1339,18 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1338
1339
|
expect(body.accepted).toBe(true);
|
|
1339
1340
|
expect(body.guardianVerification).toBe('verified');
|
|
1340
1341
|
|
|
1341
|
-
const bindingX = getGuardianBinding('
|
|
1342
|
+
const bindingX = getGuardianBinding('self', 'telegram');
|
|
1342
1343
|
expect(bindingX).not.toBeNull();
|
|
1343
1344
|
expect(bindingX!.guardianExternalUserId).toBe('user-for-asst-x');
|
|
1344
1345
|
|
|
1345
1346
|
deliverSpy.mockRestore();
|
|
1346
1347
|
});
|
|
1347
1348
|
|
|
1348
|
-
test('
|
|
1349
|
+
test('all assistant IDs share canonical scope for verification', async () => {
|
|
1349
1350
|
const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
|
|
1350
1351
|
|
|
1351
|
-
|
|
1352
|
+
// Both IDs canonicalize to 'self', so the challenge is found
|
|
1353
|
+
const { secret } = createVerificationChallenge('self', 'telegram');
|
|
1352
1354
|
|
|
1353
1355
|
const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
|
|
1354
1356
|
|
|
@@ -1361,19 +1363,19 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1361
1363
|
const body = await res.json() as Record<string, unknown>;
|
|
1362
1364
|
|
|
1363
1365
|
expect(body.accepted).toBe(true);
|
|
1364
|
-
expect(body.guardianVerification).
|
|
1366
|
+
expect(body.guardianVerification).toBe('verified');
|
|
1365
1367
|
|
|
1366
1368
|
deliverSpy.mockRestore();
|
|
1367
1369
|
});
|
|
1368
1370
|
|
|
1369
|
-
test('
|
|
1371
|
+
test('inbound with explicit assistantId does not mutate existing external bindings', async () => {
|
|
1370
1372
|
const db = getDb();
|
|
1371
1373
|
const now = Date.now();
|
|
1372
1374
|
ensureConversation('conv-existing-binding');
|
|
1373
1375
|
db.insert(externalConversationBindings).values({
|
|
1374
1376
|
conversationId: 'conv-existing-binding',
|
|
1375
1377
|
sourceChannel: 'telegram',
|
|
1376
|
-
externalChatId: 'chat-
|
|
1378
|
+
externalChatId: 'chat-existing-999',
|
|
1377
1379
|
externalUserId: 'existing-user',
|
|
1378
1380
|
createdAt: now,
|
|
1379
1381
|
updatedAt: now,
|
|
@@ -1385,7 +1387,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1385
1387
|
senderExternalUserId: 'incoming-user',
|
|
1386
1388
|
});
|
|
1387
1389
|
|
|
1388
|
-
const res = await handleChannelInbound(req,
|
|
1390
|
+
const res = await handleChannelInbound(req, noopProcessMessage, 'token', 'asst-non-self');
|
|
1389
1391
|
expect(res.status).toBe(200);
|
|
1390
1392
|
|
|
1391
1393
|
const binding = db
|
|
@@ -2665,8 +2667,10 @@ describe('background channel processing approval prompts', () => {
|
|
|
2665
2667
|
deliverPromptSpy.mockRestore();
|
|
2666
2668
|
});
|
|
2667
2669
|
|
|
2668
|
-
test('
|
|
2669
|
-
// Set up a guardian binding for a DIFFERENT user so the sender is
|
|
2670
|
+
test('trusted-contact channel turns with resolvable guardian route are interactive', async () => {
|
|
2671
|
+
// Set up a guardian binding for a DIFFERENT user so the sender is a
|
|
2672
|
+
// trusted contact (not the guardian). The guardian route is resolvable
|
|
2673
|
+
// because the binding exists — approval notifications can be delivered.
|
|
2670
2674
|
createBinding({
|
|
2671
2675
|
assistantId: 'self',
|
|
2672
2676
|
channel: 'telegram',
|
|
@@ -2701,7 +2705,9 @@ describe('background channel processing approval prompts', () => {
|
|
|
2701
2705
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
2702
2706
|
|
|
2703
2707
|
expect(processCalls.length).toBeGreaterThan(0);
|
|
2704
|
-
|
|
2708
|
+
// Trusted contacts with a resolvable guardian route should be interactive
|
|
2709
|
+
// so approval prompts can be routed to the guardian for decision.
|
|
2710
|
+
expect(processCalls[0].options?.isInteractive).toBe(true);
|
|
2705
2711
|
});
|
|
2706
2712
|
|
|
2707
2713
|
test('unverified channel turns never broadcast approval prompts', async () => {
|
|
@@ -2869,3 +2875,118 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
|
|
|
2869
2875
|
expect(unchanged!.status).toBe('pending');
|
|
2870
2876
|
});
|
|
2871
2877
|
});
|
|
2878
|
+
|
|
2879
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2880
|
+
// Trusted-contact self-approval guard (pre-row)
|
|
2881
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2882
|
+
|
|
2883
|
+
describe('trusted-contact self-approval blocked before guardian approval row exists', () => {
|
|
2884
|
+
beforeEach(() => {
|
|
2885
|
+
// Create a guardian binding so the requester resolves as trusted_contact
|
|
2886
|
+
createBinding({
|
|
2887
|
+
assistantId: 'self',
|
|
2888
|
+
channel: 'telegram',
|
|
2889
|
+
guardianExternalUserId: 'guardian-tc-selfapproval',
|
|
2890
|
+
guardianDeliveryChatId: 'guardian-tc-selfapproval-chat',
|
|
2891
|
+
});
|
|
2892
|
+
});
|
|
2893
|
+
|
|
2894
|
+
test('trusted contact cannot self-approve via conversational engine when no guardian approval row exists', async () => {
|
|
2895
|
+
const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
|
|
2896
|
+
|
|
2897
|
+
// Create the requester conversation (different user than guardian)
|
|
2898
|
+
const initReq = makeInboundRequest({
|
|
2899
|
+
content: 'init',
|
|
2900
|
+
externalChatId: 'tc-selfapproval-chat',
|
|
2901
|
+
senderExternalUserId: 'tc-selfapproval-user',
|
|
2902
|
+
});
|
|
2903
|
+
await handleChannelInbound(initReq, noopProcessMessage, 'token');
|
|
2904
|
+
|
|
2905
|
+
const db = getDb();
|
|
2906
|
+
const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
|
|
2907
|
+
const conversationId = events[0]?.conversation_id;
|
|
2908
|
+
ensureConversation(conversationId!);
|
|
2909
|
+
|
|
2910
|
+
// Register a pending interaction — but do NOT create a guardian approval
|
|
2911
|
+
// row in channelGuardianApprovalRequests. This simulates the window
|
|
2912
|
+
// between the pending confirmation being created (isInteractive=true)
|
|
2913
|
+
// and the guardian approval prompt being delivered.
|
|
2914
|
+
const sessionMock = registerPendingInteraction('req-tc-selfapproval-1', conversationId!, 'shell');
|
|
2915
|
+
|
|
2916
|
+
deliverSpy.mockClear();
|
|
2917
|
+
|
|
2918
|
+
// The conversational engine would normally classify "yes" as approve_once,
|
|
2919
|
+
// but the guard should intercept before the engine runs.
|
|
2920
|
+
const mockConversationGenerator = mock(async (_ctx: unknown) => ({
|
|
2921
|
+
disposition: 'approve_once' as const,
|
|
2922
|
+
replyText: 'Approved!',
|
|
2923
|
+
}));
|
|
2924
|
+
|
|
2925
|
+
// Trusted contact sends "yes" to try to self-approve
|
|
2926
|
+
const req = makeInboundRequest({
|
|
2927
|
+
content: 'yes',
|
|
2928
|
+
externalChatId: 'tc-selfapproval-chat',
|
|
2929
|
+
senderExternalUserId: 'tc-selfapproval-user',
|
|
2930
|
+
});
|
|
2931
|
+
const res = await handleChannelInbound(
|
|
2932
|
+
req, noopProcessMessage, 'token', 'self', undefined,
|
|
2933
|
+
undefined, mockConversationGenerator,
|
|
2934
|
+
);
|
|
2935
|
+
const body = await res.json() as Record<string, unknown>;
|
|
2936
|
+
|
|
2937
|
+
expect(body.accepted).toBe(true);
|
|
2938
|
+
// Should be blocked with assistant_turn (pending guardian notice),
|
|
2939
|
+
// NOT decision_applied
|
|
2940
|
+
expect(body.approval).toBe('assistant_turn');
|
|
2941
|
+
// The session should NOT have been resolved
|
|
2942
|
+
expect(sessionMock).not.toHaveBeenCalled();
|
|
2943
|
+
|
|
2944
|
+
// The pending interaction should still be registered (not consumed)
|
|
2945
|
+
const stillPending = pendingInteractions.get('req-tc-selfapproval-1');
|
|
2946
|
+
expect(stillPending).toBeDefined();
|
|
2947
|
+
|
|
2948
|
+
deliverSpy.mockRestore();
|
|
2949
|
+
});
|
|
2950
|
+
|
|
2951
|
+
test('trusted contact cannot self-approve via legacy parser when no guardian approval row exists', async () => {
|
|
2952
|
+
const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
|
|
2953
|
+
|
|
2954
|
+
const initReq = makeInboundRequest({
|
|
2955
|
+
content: 'init',
|
|
2956
|
+
externalChatId: 'tc-selfapproval-chat',
|
|
2957
|
+
senderExternalUserId: 'tc-selfapproval-user',
|
|
2958
|
+
});
|
|
2959
|
+
await handleChannelInbound(initReq, noopProcessMessage, 'token');
|
|
2960
|
+
|
|
2961
|
+
const db = getDb();
|
|
2962
|
+
const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
|
|
2963
|
+
const conversationId = events[0]?.conversation_id;
|
|
2964
|
+
ensureConversation(conversationId!);
|
|
2965
|
+
|
|
2966
|
+
// Register pending interaction without guardian approval row
|
|
2967
|
+
const sessionMock = registerPendingInteraction('req-tc-selfapproval-2', conversationId!, 'shell');
|
|
2968
|
+
|
|
2969
|
+
deliverSpy.mockClear();
|
|
2970
|
+
|
|
2971
|
+
// No conversational engine — falls through to legacy parser path.
|
|
2972
|
+
// "approve" would normally be parsed as an approval decision.
|
|
2973
|
+
const req = makeInboundRequest({
|
|
2974
|
+
content: 'approve',
|
|
2975
|
+
externalChatId: 'tc-selfapproval-chat',
|
|
2976
|
+
senderExternalUserId: 'tc-selfapproval-user',
|
|
2977
|
+
});
|
|
2978
|
+
const res = await handleChannelInbound(req, noopProcessMessage, 'token');
|
|
2979
|
+
const body = await res.json() as Record<string, unknown>;
|
|
2980
|
+
|
|
2981
|
+
expect(body.accepted).toBe(true);
|
|
2982
|
+
// Should be blocked, not decision_applied
|
|
2983
|
+
expect(body.approval).toBe('assistant_turn');
|
|
2984
|
+
expect(sessionMock).not.toHaveBeenCalled();
|
|
2985
|
+
|
|
2986
|
+
// Pending interaction should still exist
|
|
2987
|
+
const stillPending = pendingInteractions.get('req-tc-selfapproval-2');
|
|
2988
|
+
expect(stillPending).toBeDefined();
|
|
2989
|
+
|
|
2990
|
+
deliverSpy.mockRestore();
|
|
2991
|
+
});
|
|
2992
|
+
});
|