@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
|
@@ -31,41 +31,15 @@ function hasIngressConfigured(): boolean {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function getAssistantMappedPhoneNumber(
|
|
35
|
-
smsConfig: Record<string, unknown>,
|
|
36
|
-
assistantId?: string,
|
|
37
|
-
): string | undefined {
|
|
38
|
-
if (!assistantId) return undefined;
|
|
39
|
-
const mapping = (smsConfig.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
40
|
-
return mapping[assistantId];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function hasAnyAssistantMappedPhoneNumber(smsConfig: Record<string, unknown>): boolean {
|
|
44
|
-
const mapping = (smsConfig.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
45
|
-
return Object.keys(mapping).length > 0;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function hasAnyAssistantMappedPhoneNumberSafe(): boolean {
|
|
49
|
-
try {
|
|
50
|
-
const raw = loadRawConfig();
|
|
51
|
-
const smsConfig = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
52
|
-
return hasAnyAssistantMappedPhoneNumber(smsConfig);
|
|
53
|
-
} catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
34
|
/**
|
|
59
35
|
* Resolve SMS from-number with canonical precedence:
|
|
60
|
-
*
|
|
36
|
+
* env override -> config sms.phoneNumber -> secure key fallback.
|
|
61
37
|
*/
|
|
62
|
-
function resolveSmsPhoneNumber(
|
|
38
|
+
function resolveSmsPhoneNumber(): string {
|
|
63
39
|
try {
|
|
64
40
|
const raw = loadRawConfig();
|
|
65
41
|
const smsConfig = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
66
|
-
|
|
67
|
-
return mapped
|
|
68
|
-
|| getTwilioPhoneNumberEnv()
|
|
42
|
+
return getTwilioPhoneNumberEnv()
|
|
69
43
|
|| (smsConfig.phoneNumber as string)
|
|
70
44
|
|| getSecureKey('credential:twilio:phone_number')
|
|
71
45
|
|| '';
|
|
@@ -78,7 +52,7 @@ function resolveSmsPhoneNumber(assistantId?: string): string {
|
|
|
78
52
|
|
|
79
53
|
const smsProbe: ChannelProbe = {
|
|
80
54
|
channel: 'sms',
|
|
81
|
-
runLocalChecks(
|
|
55
|
+
runLocalChecks(): ReadinessCheckResult[] {
|
|
82
56
|
const results: ReadinessCheckResult[] = [];
|
|
83
57
|
|
|
84
58
|
const hasCreds = hasTwilioCredentials();
|
|
@@ -90,18 +64,14 @@ const smsProbe: ChannelProbe = {
|
|
|
90
64
|
: 'Twilio Account SID and Auth Token are not configured',
|
|
91
65
|
});
|
|
92
66
|
|
|
93
|
-
const resolvedNumber = resolveSmsPhoneNumber(
|
|
94
|
-
const hasPhone = !!resolvedNumber
|
|
67
|
+
const resolvedNumber = resolveSmsPhoneNumber();
|
|
68
|
+
const hasPhone = !!resolvedNumber;
|
|
95
69
|
results.push({
|
|
96
70
|
name: 'phone_number',
|
|
97
71
|
passed: hasPhone,
|
|
98
72
|
message: hasPhone
|
|
99
|
-
?
|
|
100
|
-
|
|
101
|
-
: 'Phone number is assigned')
|
|
102
|
-
: (context?.assistantId
|
|
103
|
-
? `No phone number assigned for assistant ${context.assistantId}`
|
|
104
|
-
: 'No phone number assigned'),
|
|
73
|
+
? 'Phone number is assigned'
|
|
74
|
+
: 'No phone number assigned',
|
|
105
75
|
});
|
|
106
76
|
|
|
107
77
|
const hasIngress = hasIngressConfigured();
|
|
@@ -115,14 +85,14 @@ const smsProbe: ChannelProbe = {
|
|
|
115
85
|
|
|
116
86
|
return results;
|
|
117
87
|
},
|
|
118
|
-
async runRemoteChecks(
|
|
88
|
+
async runRemoteChecks(): Promise<ReadinessCheckResult[]> {
|
|
119
89
|
if (!hasTwilioCredentials()) return [];
|
|
120
90
|
|
|
121
91
|
const accountSid = getSecureKey('credential:twilio:account_sid');
|
|
122
92
|
const authToken = getSecureKey('credential:twilio:auth_token');
|
|
123
93
|
if (!accountSid || !authToken) return [];
|
|
124
94
|
|
|
125
|
-
const phoneNumber = resolveSmsPhoneNumber(
|
|
95
|
+
const phoneNumber = resolveSmsPhoneNumber();
|
|
126
96
|
if (!phoneNumber) return [];
|
|
127
97
|
|
|
128
98
|
// Only toll-free numbers need verification checks
|
|
@@ -170,18 +140,16 @@ const smsProbe: ChannelProbe = {
|
|
|
170
140
|
|
|
171
141
|
/**
|
|
172
142
|
* Resolve voice from-number with the same precedence as SMS:
|
|
173
|
-
*
|
|
143
|
+
* env override -> config sms.phoneNumber -> secure key fallback.
|
|
174
144
|
*
|
|
175
145
|
* Voice and SMS share the same Twilio phone number infrastructure, so the
|
|
176
146
|
* resolution logic is identical to resolveSmsPhoneNumber.
|
|
177
147
|
*/
|
|
178
|
-
function resolveVoicePhoneNumber(
|
|
148
|
+
function resolveVoicePhoneNumber(): string {
|
|
179
149
|
try {
|
|
180
150
|
const raw = loadRawConfig();
|
|
181
151
|
const smsConfig = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
182
|
-
|
|
183
|
-
return mapped
|
|
184
|
-
|| getTwilioPhoneNumberEnv()
|
|
152
|
+
return getTwilioPhoneNumberEnv()
|
|
185
153
|
|| (smsConfig.phoneNumber as string)
|
|
186
154
|
|| getSecureKey('credential:twilio:phone_number')
|
|
187
155
|
|| '';
|
|
@@ -194,7 +162,7 @@ function resolveVoicePhoneNumber(assistantId?: string): string {
|
|
|
194
162
|
|
|
195
163
|
const voiceProbe: ChannelProbe = {
|
|
196
164
|
channel: 'voice',
|
|
197
|
-
runLocalChecks(
|
|
165
|
+
runLocalChecks(): ReadinessCheckResult[] {
|
|
198
166
|
const results: ReadinessCheckResult[] = [];
|
|
199
167
|
|
|
200
168
|
const hasCreds = hasTwilioCredentials();
|
|
@@ -206,18 +174,14 @@ const voiceProbe: ChannelProbe = {
|
|
|
206
174
|
: 'Twilio Account SID and Auth Token are not configured',
|
|
207
175
|
});
|
|
208
176
|
|
|
209
|
-
const resolvedNumber = resolveVoicePhoneNumber(
|
|
210
|
-
const hasPhone = !!resolvedNumber
|
|
177
|
+
const resolvedNumber = resolveVoicePhoneNumber();
|
|
178
|
+
const hasPhone = !!resolvedNumber;
|
|
211
179
|
results.push({
|
|
212
180
|
name: 'phone_number',
|
|
213
181
|
passed: hasPhone,
|
|
214
182
|
message: hasPhone
|
|
215
|
-
?
|
|
216
|
-
|
|
217
|
-
: 'Phone number is assigned for voice calls')
|
|
218
|
-
: (context?.assistantId
|
|
219
|
-
? `No phone number assigned for assistant ${context.assistantId}`
|
|
220
|
-
: 'No phone number assigned for voice calls'),
|
|
183
|
+
? 'Phone number is assigned for voice calls'
|
|
184
|
+
: 'No phone number assigned for voice calls',
|
|
221
185
|
});
|
|
222
186
|
|
|
223
187
|
const hasIngress = hasIngressConfigured();
|
|
@@ -290,7 +254,6 @@ export class ChannelReadinessService {
|
|
|
290
254
|
async getReadiness(
|
|
291
255
|
channel?: ChannelId,
|
|
292
256
|
includeRemote?: boolean,
|
|
293
|
-
assistantId?: string,
|
|
294
257
|
): Promise<ChannelReadinessSnapshot[]> {
|
|
295
258
|
const channels = channel
|
|
296
259
|
? [channel]
|
|
@@ -304,14 +267,14 @@ export class ChannelReadinessService {
|
|
|
304
267
|
continue;
|
|
305
268
|
}
|
|
306
269
|
|
|
307
|
-
const probeContext: ChannelProbeContext = {
|
|
270
|
+
const probeContext: ChannelProbeContext = {};
|
|
308
271
|
const localChecks = probe.runLocalChecks(probeContext);
|
|
309
272
|
let remoteChecks: ReadinessCheckResult[] | undefined;
|
|
310
273
|
let remoteChecksFreshlyFetched = false;
|
|
311
274
|
let remoteChecksAffectReadiness = false;
|
|
312
275
|
let stale = false;
|
|
313
276
|
|
|
314
|
-
const cacheKey = this.snapshotCacheKey(ch
|
|
277
|
+
const cacheKey = this.snapshotCacheKey(ch);
|
|
315
278
|
const cached = this.snapshots.get(cacheKey);
|
|
316
279
|
const now = Date.now();
|
|
317
280
|
|
|
@@ -372,11 +335,7 @@ export class ChannelReadinessService {
|
|
|
372
335
|
}
|
|
373
336
|
|
|
374
337
|
/** Clear cached snapshot for a specific channel, forcing re-evaluation on next call. */
|
|
375
|
-
invalidateChannel(channel: ChannelId
|
|
376
|
-
if (assistantId) {
|
|
377
|
-
this.snapshots.delete(this.snapshotCacheKey(channel, assistantId));
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
338
|
+
invalidateChannel(channel: ChannelId): void {
|
|
380
339
|
const prefix = `${channel}::`;
|
|
381
340
|
for (const key of this.snapshots.keys()) {
|
|
382
341
|
if (key.startsWith(prefix)) {
|
|
@@ -401,8 +360,8 @@ export class ChannelReadinessService {
|
|
|
401
360
|
};
|
|
402
361
|
}
|
|
403
362
|
|
|
404
|
-
private snapshotCacheKey(channel: ChannelId
|
|
405
|
-
return `${channel}
|
|
363
|
+
private snapshotCacheKey(channel: ChannelId): string {
|
|
364
|
+
return `${channel}::__default__`;
|
|
406
365
|
}
|
|
407
366
|
}
|
|
408
367
|
|
|
@@ -22,10 +22,9 @@ export interface ChannelReadinessSnapshot {
|
|
|
22
22
|
remoteChecks?: ReadinessCheckResult[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** Optional probe context for
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
25
|
+
/** Optional probe context for readiness checks. */
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
27
|
+
export interface ChannelProbeContext {}
|
|
29
28
|
|
|
30
29
|
/** Probe interface that channels implement to provide readiness checks. */
|
|
31
30
|
export interface ChannelProbe {
|
|
@@ -7,6 +7,7 @@ import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.
|
|
|
7
7
|
import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
|
|
8
8
|
import { getLogger } from '../util/logger.js';
|
|
9
9
|
import { deliverReplyViaCallback } from './channel-reply-delivery.js';
|
|
10
|
+
import { resolveRoutingStateFromRuntime } from './guardian-context-resolver.js';
|
|
10
11
|
import type { MessageProcessor } from './http-types.js';
|
|
11
12
|
|
|
12
13
|
const log = getLogger('runtime-http');
|
|
@@ -129,7 +130,9 @@ export async function sweepFailedEvents(
|
|
|
129
130
|
},
|
|
130
131
|
assistantId,
|
|
131
132
|
guardianContext,
|
|
132
|
-
isInteractive: guardianContext
|
|
133
|
+
isInteractive: guardianContext
|
|
134
|
+
? resolveRoutingStateFromRuntime(guardianContext).promptWaitingAllowed
|
|
135
|
+
: false,
|
|
133
136
|
},
|
|
134
137
|
sourceChannel,
|
|
135
138
|
sourceInterface,
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge trusted-contact confirmation_request events to guardian.question notifications.
|
|
3
|
+
*
|
|
4
|
+
* When a trusted-contact channel session creates a confirmation_request (tool approval),
|
|
5
|
+
* this helper emits a guardian.question notification signal and persists canonical
|
|
6
|
+
* delivery rows to guardian destinations (Telegram/SMS/Vellum), enabling the guardian
|
|
7
|
+
* to approve via callback/request-code path.
|
|
8
|
+
*
|
|
9
|
+
* Modeled after the tool-grant-request-helper pattern. Designed to be called from
|
|
10
|
+
* both the daemon event registrar (server.ts) and the HTTP hub publisher
|
|
11
|
+
* (conversation-routes.ts) — the two paths that create confirmation_request
|
|
12
|
+
* canonical records.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
16
|
+
import {
|
|
17
|
+
type CanonicalGuardianRequest,
|
|
18
|
+
createCanonicalGuardianDelivery,
|
|
19
|
+
} from '../memory/canonical-guardian-store.js';
|
|
20
|
+
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
21
|
+
import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
|
|
22
|
+
import { getLogger } from '../util/logger.js';
|
|
23
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
24
|
+
import { getGuardianBinding } from './channel-guardian-service.js';
|
|
25
|
+
|
|
26
|
+
const log = getLogger('confirmation-request-guardian-bridge');
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Types
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export interface BridgeConfirmationRequestParams {
|
|
33
|
+
/** The canonical guardian request already persisted for this confirmation_request. */
|
|
34
|
+
canonicalRequest: CanonicalGuardianRequest;
|
|
35
|
+
/** Guardian runtime context from the session. */
|
|
36
|
+
guardianContext: GuardianRuntimeContext;
|
|
37
|
+
/** Conversation ID where the confirmation_request was emitted. */
|
|
38
|
+
conversationId: string;
|
|
39
|
+
/** Tool name from the confirmation_request. */
|
|
40
|
+
toolName: string;
|
|
41
|
+
/** Logical assistant ID (defaults to 'self'). */
|
|
42
|
+
assistantId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BridgeConfirmationRequestResult =
|
|
46
|
+
| { bridged: true; signalId: string }
|
|
47
|
+
| { skipped: true; reason: 'not_trusted_contact' | 'no_guardian_binding' | 'missing_guardian_identity' | 'binding_identity_mismatch' };
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Helper
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Bridge a trusted-contact confirmation_request to a guardian.question notification.
|
|
55
|
+
*
|
|
56
|
+
* Only emits when the session belongs to a trusted-contact actor with a
|
|
57
|
+
* resolvable guardian binding. Guardian and unknown actors are skipped — guardians
|
|
58
|
+
* self-approve, and unknown actors are already fail-closed by the routing layer.
|
|
59
|
+
*
|
|
60
|
+
* Fire-and-forget safe: notification emission errors are logged but not propagated.
|
|
61
|
+
*/
|
|
62
|
+
export function bridgeConfirmationRequestToGuardian(
|
|
63
|
+
params: BridgeConfirmationRequestParams,
|
|
64
|
+
): BridgeConfirmationRequestResult {
|
|
65
|
+
const {
|
|
66
|
+
canonicalRequest,
|
|
67
|
+
guardianContext,
|
|
68
|
+
conversationId,
|
|
69
|
+
toolName,
|
|
70
|
+
assistantId = DAEMON_INTERNAL_ASSISTANT_ID,
|
|
71
|
+
} = params;
|
|
72
|
+
|
|
73
|
+
// Only bridge for trusted-contact sessions. Guardians self-approve and
|
|
74
|
+
// unknown actors are fail-closed by the routing layer.
|
|
75
|
+
if (guardianContext.trustClass !== 'trusted_contact') {
|
|
76
|
+
return { skipped: true, reason: 'not_trusted_contact' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!guardianContext.guardianExternalUserId) {
|
|
80
|
+
log.debug(
|
|
81
|
+
{ conversationId, sourceChannel: guardianContext.sourceChannel },
|
|
82
|
+
'Skipping guardian bridge: no guardian identity on trusted-contact context',
|
|
83
|
+
);
|
|
84
|
+
return { skipped: true, reason: 'missing_guardian_identity' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sourceChannel = guardianContext.sourceChannel;
|
|
88
|
+
const binding = getGuardianBinding(assistantId, sourceChannel);
|
|
89
|
+
if (!binding) {
|
|
90
|
+
log.debug(
|
|
91
|
+
{ sourceChannel, assistantId },
|
|
92
|
+
'No guardian binding for confirmation request bridge',
|
|
93
|
+
);
|
|
94
|
+
return { skipped: true, reason: 'no_guardian_binding' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate that the binding's guardian identity matches the canonical request's
|
|
98
|
+
// guardian identity. A mismatch can occur if a guardian rebind happens between
|
|
99
|
+
// message ingress and confirmation emission — sending the notification to the
|
|
100
|
+
// new binding would leak requester/tool metadata to the wrong recipient.
|
|
101
|
+
//
|
|
102
|
+
// Both sides are canonicalized before comparison because the canonical request
|
|
103
|
+
// value was normalized by resolveGuardianContext() while the binding stores the
|
|
104
|
+
// raw identity. On phone channels the same guardian can have format variance
|
|
105
|
+
// (e.g. "+1 555-123-4567" vs "+15551234567") that would cause a false mismatch.
|
|
106
|
+
const canonicalBindingId = canonicalizeInboundIdentity(sourceChannel, binding.guardianExternalUserId);
|
|
107
|
+
const canonicalRequestId = canonicalRequest.guardianExternalUserId
|
|
108
|
+
? canonicalizeInboundIdentity(sourceChannel, canonicalRequest.guardianExternalUserId)
|
|
109
|
+
: null;
|
|
110
|
+
if (
|
|
111
|
+
canonicalRequestId &&
|
|
112
|
+
canonicalBindingId !== canonicalRequestId
|
|
113
|
+
) {
|
|
114
|
+
log.warn(
|
|
115
|
+
{
|
|
116
|
+
sourceChannel,
|
|
117
|
+
assistantId,
|
|
118
|
+
bindingGuardianId: binding.guardianExternalUserId,
|
|
119
|
+
expectedGuardianId: canonicalRequest.guardianExternalUserId,
|
|
120
|
+
requestId: canonicalRequest.id,
|
|
121
|
+
},
|
|
122
|
+
'Guardian binding identity does not match canonical request guardian — skipping notification to prevent misrouting',
|
|
123
|
+
);
|
|
124
|
+
return { skipped: true, reason: 'binding_identity_mismatch' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const senderLabel = guardianContext.requesterIdentifier
|
|
128
|
+
|| guardianContext.requesterExternalUserId
|
|
129
|
+
|| 'unknown';
|
|
130
|
+
|
|
131
|
+
const questionText = `Tool approval request: ${toolName}`;
|
|
132
|
+
|
|
133
|
+
// Emit guardian.question notification so the guardian is alerted.
|
|
134
|
+
const signalPromise = emitNotificationSignal({
|
|
135
|
+
sourceEventName: 'guardian.question',
|
|
136
|
+
sourceChannel,
|
|
137
|
+
sourceSessionId: conversationId,
|
|
138
|
+
assistantId,
|
|
139
|
+
attentionHints: {
|
|
140
|
+
requiresAction: true,
|
|
141
|
+
urgency: 'high',
|
|
142
|
+
isAsyncBackground: false,
|
|
143
|
+
visibleInSourceNow: false,
|
|
144
|
+
},
|
|
145
|
+
contextPayload: {
|
|
146
|
+
requestKind: 'tool_approval' as const,
|
|
147
|
+
requestId: canonicalRequest.id,
|
|
148
|
+
requestCode: canonicalRequest.requestCode ?? canonicalRequest.id.slice(0, 6).toUpperCase(),
|
|
149
|
+
sourceChannel,
|
|
150
|
+
requesterExternalUserId: guardianContext.requesterExternalUserId,
|
|
151
|
+
requesterChatId: guardianContext.requesterChatId ?? null,
|
|
152
|
+
requesterIdentifier: senderLabel,
|
|
153
|
+
toolName,
|
|
154
|
+
questionText,
|
|
155
|
+
},
|
|
156
|
+
dedupeKey: `tc-confirmation-request:${canonicalRequest.id}`,
|
|
157
|
+
onThreadCreated: (info) => {
|
|
158
|
+
createCanonicalGuardianDelivery({
|
|
159
|
+
requestId: canonicalRequest.id,
|
|
160
|
+
destinationChannel: 'vellum',
|
|
161
|
+
destinationConversationId: info.conversationId,
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Record channel deliveries from the notification pipeline (fire-and-forget).
|
|
167
|
+
void signalPromise.then((signalResult) => {
|
|
168
|
+
for (const result of signalResult.deliveryResults) {
|
|
169
|
+
if (result.channel === 'vellum') continue; // handled in onThreadCreated
|
|
170
|
+
if (result.channel !== 'telegram' && result.channel !== 'sms') continue;
|
|
171
|
+
createCanonicalGuardianDelivery({
|
|
172
|
+
requestId: canonicalRequest.id,
|
|
173
|
+
destinationChannel: result.channel,
|
|
174
|
+
destinationChatId: result.destination.length > 0 ? result.destination : undefined,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}).catch((err) => {
|
|
178
|
+
log.warn({ err, requestId: canonicalRequest.id }, 'Failed to record channel deliveries for guardian bridge');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
log.info(
|
|
182
|
+
{
|
|
183
|
+
sourceChannel,
|
|
184
|
+
requesterExternalUserId: guardianContext.requesterExternalUserId,
|
|
185
|
+
toolName,
|
|
186
|
+
requestId: canonicalRequest.id,
|
|
187
|
+
requestCode: canonicalRequest.requestCode,
|
|
188
|
+
},
|
|
189
|
+
'Guardian notified of trusted-contact confirmation request',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Return the signal ID synchronously from the promise-producing call.
|
|
193
|
+
// The actual signal ID is not available until the promise resolves, but
|
|
194
|
+
// callers only need to know it was bridged — the ID is for diagnostics.
|
|
195
|
+
// We use the canonical request ID as a stable correlation key.
|
|
196
|
+
return { bridged: true, signalId: canonicalRequest.id };
|
|
197
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guardian action follow-up executor.
|
|
3
3
|
*
|
|
4
|
-
* After the conversation engine
|
|
4
|
+
* After the conversation engine classifies the guardian's reply as
|
|
5
5
|
* `call_back` or `message_back` and transitions the follow-up state to
|
|
6
6
|
* `dispatching`, this module executes the actual action:
|
|
7
7
|
*
|
|
@@ -62,6 +62,88 @@ export function resolveGuardianContext(input: ResolveGuardianContextInput): Guar
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Routing-state helper
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Routing state for a channel actor turn.
|
|
71
|
+
*
|
|
72
|
+
* Determines whether a turn should be treated as interactive (the caller
|
|
73
|
+
* can be kept waiting for a guardian to respond to an approval prompt) by
|
|
74
|
+
* combining trust class with guardian route resolvability.
|
|
75
|
+
*
|
|
76
|
+
* A guardian route is "resolvable" when a verified guardian binding exists
|
|
77
|
+
* for the channel — meaning there is a concrete destination to deliver
|
|
78
|
+
* approval notifications to. Without a resolvable guardian route, entering
|
|
79
|
+
* an interactive wait (up to 300s) is a dead-end: no guardian will ever
|
|
80
|
+
* see the prompt.
|
|
81
|
+
*/
|
|
82
|
+
export interface RoutingState {
|
|
83
|
+
/** Whether the actor's trust class alone permits interactive waits. */
|
|
84
|
+
canBeInteractive: boolean;
|
|
85
|
+
/** Whether a verified guardian destination exists for this channel. */
|
|
86
|
+
guardianRouteResolvable: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Whether the turn should actually enter an interactive prompt wait.
|
|
89
|
+
* True only when the actor can be interactive AND a guardian route is
|
|
90
|
+
* resolvable. This is the canonical decision used by processMessage.
|
|
91
|
+
*/
|
|
92
|
+
promptWaitingAllowed: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compute the routing state for a channel actor turn.
|
|
97
|
+
*
|
|
98
|
+
* Guardian actors are always interactive (they self-approve).
|
|
99
|
+
* Trusted contacts are only interactive when a guardian binding exists
|
|
100
|
+
* to receive approval notifications. Unknown actors are never interactive.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveRoutingState(ctx: GuardianContext): RoutingState {
|
|
103
|
+
const isGuardian = ctx.trustClass === 'guardian';
|
|
104
|
+
const isTrustedContact = ctx.trustClass === 'trusted_contact';
|
|
105
|
+
|
|
106
|
+
// Guardians self-approve — they are always interactive and route-resolvable.
|
|
107
|
+
if (isGuardian) {
|
|
108
|
+
return {
|
|
109
|
+
canBeInteractive: true,
|
|
110
|
+
guardianRouteResolvable: true,
|
|
111
|
+
promptWaitingAllowed: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Trusted contacts can be interactive only if a guardian destination
|
|
116
|
+
// exists. The guardian binding populates guardianExternalUserId during
|
|
117
|
+
// trust resolution; its presence means there is a verified guardian
|
|
118
|
+
// to route approval notifications to.
|
|
119
|
+
const guardianRouteResolvable = !!ctx.guardianExternalUserId;
|
|
120
|
+
if (isTrustedContact) {
|
|
121
|
+
return {
|
|
122
|
+
canBeInteractive: true,
|
|
123
|
+
guardianRouteResolvable,
|
|
124
|
+
promptWaitingAllowed: guardianRouteResolvable,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Unknown actors are never interactive.
|
|
129
|
+
return {
|
|
130
|
+
canBeInteractive: false,
|
|
131
|
+
guardianRouteResolvable: !!ctx.guardianExternalUserId,
|
|
132
|
+
promptWaitingAllowed: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convenience: compute routing state from a GuardianRuntimeContext
|
|
138
|
+
* (the shape persisted in stored payloads and used by the retry sweep).
|
|
139
|
+
*/
|
|
140
|
+
export function resolveRoutingStateFromRuntime(ctx: GuardianRuntimeContext): RoutingState {
|
|
141
|
+
return resolveRoutingState({
|
|
142
|
+
trustClass: ctx.trustClass,
|
|
143
|
+
guardianExternalUserId: ctx.guardianExternalUserId,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
65
147
|
export function toGuardianRuntimeContext(sourceChannel: ChannelId, ctx: GuardianContext): GuardianRuntimeContext {
|
|
66
148
|
return {
|
|
67
149
|
sourceChannel,
|
|
@@ -16,7 +16,8 @@ import { sendMessage as sendSms } from '../messaging/providers/sms/client.js';
|
|
|
16
16
|
import { getCredentialMetadata } from '../tools/credentials/metadata-store.js';
|
|
17
17
|
import { getLogger } from '../util/logger.js';
|
|
18
18
|
import { normalizePhoneNumber } from '../util/phone.js';
|
|
19
|
-
import {
|
|
19
|
+
import { readHttpToken } from '../util/platform.js';
|
|
20
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
20
21
|
import {
|
|
21
22
|
countRecentSendsToDestination,
|
|
22
23
|
createOutboundSession,
|
|
@@ -93,7 +94,6 @@ function getTelegramBotUsername(): string | undefined {
|
|
|
93
94
|
|
|
94
95
|
export interface StartOutboundParams {
|
|
95
96
|
channel: ChannelId;
|
|
96
|
-
assistantId?: string;
|
|
97
97
|
destination?: string;
|
|
98
98
|
rebind?: boolean;
|
|
99
99
|
/** Origin conversation ID so completion/failure pointers can route back. */
|
|
@@ -102,14 +102,12 @@ export interface StartOutboundParams {
|
|
|
102
102
|
|
|
103
103
|
export interface ResendOutboundParams {
|
|
104
104
|
channel: ChannelId;
|
|
105
|
-
assistantId?: string;
|
|
106
105
|
/** Origin conversation ID so completion/failure pointers can route back on resend. */
|
|
107
106
|
originConversationId?: string;
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
export interface CancelOutboundParams {
|
|
111
110
|
channel: ChannelId;
|
|
112
|
-
assistantId?: string;
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
/**
|
|
@@ -243,7 +241,7 @@ function initiateGuardianVoiceCall(
|
|
|
243
241
|
// ---------------------------------------------------------------------------
|
|
244
242
|
|
|
245
243
|
export function startOutbound(params: StartOutboundParams): OutboundActionResult {
|
|
246
|
-
const assistantId =
|
|
244
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
247
245
|
const channel = params.channel;
|
|
248
246
|
const originConversationId = params.originConversationId;
|
|
249
247
|
|
|
@@ -541,7 +539,7 @@ function startOutboundVoice(
|
|
|
541
539
|
// ---------------------------------------------------------------------------
|
|
542
540
|
|
|
543
541
|
export function resendOutbound(params: ResendOutboundParams): OutboundActionResult {
|
|
544
|
-
const assistantId =
|
|
542
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
545
543
|
const channel = params.channel;
|
|
546
544
|
const originConversationId = params.originConversationId;
|
|
547
545
|
|
|
@@ -707,7 +705,7 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
707
705
|
// ---------------------------------------------------------------------------
|
|
708
706
|
|
|
709
707
|
export function cancelOutbound(params: CancelOutboundParams): OutboundActionResult {
|
|
710
|
-
const assistantId =
|
|
708
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
711
709
|
const channel = params.channel;
|
|
712
710
|
|
|
713
711
|
const session = findActiveSession(assistantId, channel);
|