@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
|
@@ -34,7 +34,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
34
34
|
getDbPath: () => join(testDir, 'test.db'),
|
|
35
35
|
getLogPath: () => join(testDir, 'test.log'),
|
|
36
36
|
ensureDataDir: () => {},
|
|
37
|
-
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
38
37
|
readHttpToken: () => 'test-bearer-token',
|
|
39
38
|
}));
|
|
40
39
|
|
|
@@ -82,6 +81,7 @@ mock.module('../runtime/approval-message-composer.js', () => ({
|
|
|
82
81
|
composeApprovalMessageGenerative: async () => 'mock generative message',
|
|
83
82
|
}));
|
|
84
83
|
|
|
84
|
+
import { getResolver } from '../approvals/guardian-request-resolvers.js';
|
|
85
85
|
import {
|
|
86
86
|
createApprovalRequest,
|
|
87
87
|
createBinding,
|
|
@@ -489,6 +489,16 @@ describe('trusted contact activated notification signal', () => {
|
|
|
489
489
|
expect(activatedSignals.length).toBe(0);
|
|
490
490
|
});
|
|
491
491
|
|
|
492
|
+
test('voice access_request resolver has registered handler for access_request kind', () => {
|
|
493
|
+
// The access_request resolver is registered during module load. When the
|
|
494
|
+
// source channel is 'voice', it should directly activate the member via
|
|
495
|
+
// upsertMember (no verification session). This test validates the resolver
|
|
496
|
+
// is registered and accessible.
|
|
497
|
+
const resolver = getResolver('access_request');
|
|
498
|
+
expect(resolver).toBeDefined();
|
|
499
|
+
expect(resolver!.kind).toBe('access_request');
|
|
500
|
+
});
|
|
501
|
+
|
|
492
502
|
test('member is persisted BEFORE activated signal is emitted', async () => {
|
|
493
503
|
// Set up a guardian binding
|
|
494
504
|
createBinding({
|
|
@@ -29,7 +29,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
29
29
|
getDbPath: () => join(testDir, 'test.db'),
|
|
30
30
|
getLogPath: () => join(testDir, 'test.log'),
|
|
31
31
|
ensureDataDir: () => {},
|
|
32
|
-
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
33
32
|
readHttpToken: () => 'test-bearer-token',
|
|
34
33
|
}));
|
|
35
34
|
|
|
@@ -32,7 +32,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
32
32
|
getDbPath: () => join(testDir, 'test.db'),
|
|
33
33
|
getLogPath: () => join(testDir, 'test.log'),
|
|
34
34
|
ensureDataDir: () => {},
|
|
35
|
-
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
36
35
|
readHttpToken: () => 'test-bearer-token',
|
|
37
36
|
}));
|
|
38
37
|
|
|
@@ -86,25 +86,14 @@ describe('twilio-config', () => {
|
|
|
86
86
|
expect(config.phoneNumber).toBe('+15558888888');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
test('
|
|
89
|
+
test('returns global phone number when assistantPhoneNumbers mapping exists', () => {
|
|
90
90
|
mockLoadConfigResult = {
|
|
91
91
|
sms: {
|
|
92
92
|
phoneNumber: '+15551234567',
|
|
93
93
|
assistantPhoneNumbers: { 'ast-1': '+15557777777' },
|
|
94
94
|
},
|
|
95
95
|
};
|
|
96
|
-
const config = getTwilioConfig(
|
|
97
|
-
expect(config.phoneNumber).toBe('+15557777777');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('falls back to global phone number when assistant has no dedicated number', () => {
|
|
101
|
-
mockLoadConfigResult = {
|
|
102
|
-
sms: {
|
|
103
|
-
phoneNumber: '+15551234567',
|
|
104
|
-
assistantPhoneNumbers: { 'ast-1': '+15557777777' },
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
const config = getTwilioConfig('ast-unknown');
|
|
96
|
+
const config = getTwilioConfig();
|
|
108
97
|
expect(config.phoneNumber).toBe('+15551234567');
|
|
109
98
|
});
|
|
110
99
|
});
|
|
@@ -704,19 +704,20 @@ describe('twilio webhook routes', () => {
|
|
|
704
704
|
expect(res.status).toBe(400);
|
|
705
705
|
});
|
|
706
706
|
|
|
707
|
-
test('inbound webhook
|
|
707
|
+
test('inbound webhook creates session with internal scope assistantId', async () => {
|
|
708
708
|
const req = makeInboundVoiceRequest({
|
|
709
709
|
CallSid: 'CA_inbound_assist_1',
|
|
710
710
|
From: '+14155551234',
|
|
711
711
|
To: '+15550001111',
|
|
712
712
|
});
|
|
713
713
|
|
|
714
|
-
const res = await handleVoiceWebhook(req
|
|
714
|
+
const res = await handleVoiceWebhook(req);
|
|
715
715
|
|
|
716
716
|
expect(res.status).toBe(200);
|
|
717
717
|
const session = getCallSessionByCallSid('CA_inbound_assist_1');
|
|
718
718
|
expect(session).not.toBeNull();
|
|
719
|
-
|
|
719
|
+
// Daemon always uses internal scope — external assistant IDs are not leaked into session state.
|
|
720
|
+
expect(session!.assistantId).toBe('self');
|
|
720
721
|
});
|
|
721
722
|
|
|
722
723
|
test('outbound call flow remains non-regressed with callSessionId present', async () => {
|
|
@@ -53,7 +53,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
53
53
|
getInterfacesDir: () => '',
|
|
54
54
|
getClipboardCommand: () => null,
|
|
55
55
|
readLockfile: () => null,
|
|
56
|
-
normalizeAssistantId: (id: string) => id,
|
|
57
56
|
writeLockfile: () => {},
|
|
58
57
|
readPlatformToken: () => null,
|
|
59
58
|
readSessionToken: () => null,
|
package/src/agent/loop.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type GuardianApprovalRequest,
|
|
36
36
|
updateApprovalDecision,
|
|
37
37
|
} from '../memory/channel-guardian-store.js';
|
|
38
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
38
39
|
import type {
|
|
39
40
|
ApprovalAction,
|
|
40
41
|
ApprovalDecisionResult,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
type ActorContext,
|
|
53
54
|
type ChannelDeliveryContext,
|
|
54
55
|
getResolver,
|
|
56
|
+
type ResolverEmissionContext,
|
|
55
57
|
} from './guardian-request-resolvers.js';
|
|
56
58
|
|
|
57
59
|
const log = getLogger('guardian-decision-primitive');
|
|
@@ -233,7 +235,7 @@ export function mintCanonicalRequestGrant(params: {
|
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
const result = mintGrantFromDecision({
|
|
236
|
-
assistantId:
|
|
238
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
237
239
|
scopeMode: 'tool_signature',
|
|
238
240
|
toolName: request.toolName,
|
|
239
241
|
inputDigest: request.inputDigest,
|
|
@@ -294,10 +296,12 @@ export interface ApplyCanonicalGuardianDecisionParams {
|
|
|
294
296
|
userText?: string;
|
|
295
297
|
/** Optional channel delivery context — present when the decision arrived via a channel message. */
|
|
296
298
|
channelDeliveryContext?: ChannelDeliveryContext;
|
|
299
|
+
/** Optional emission context threaded to handleConfirmationResponse for correct source attribution. */
|
|
300
|
+
emissionContext?: ResolverEmissionContext;
|
|
297
301
|
}
|
|
298
302
|
|
|
299
303
|
export type CanonicalDecisionResult =
|
|
300
|
-
| { applied: true; requestId: string; grantMinted: boolean; resolverFailed?: boolean; resolverFailureReason?: string }
|
|
304
|
+
| { applied: true; requestId: string; grantMinted: boolean; resolverFailed?: boolean; resolverFailureReason?: string; resolverReplyText?: string }
|
|
301
305
|
| { applied: false; reason: 'not_found' | 'already_resolved' | 'identity_mismatch' | 'invalid_action' | 'expired'; detail?: string };
|
|
302
306
|
|
|
303
307
|
/**
|
|
@@ -318,7 +322,7 @@ export type CanonicalDecisionResult =
|
|
|
318
322
|
export async function applyCanonicalGuardianDecision(
|
|
319
323
|
params: ApplyCanonicalGuardianDecisionParams,
|
|
320
324
|
): Promise<CanonicalDecisionResult> {
|
|
321
|
-
const { requestId, action, actorContext, userText, channelDeliveryContext } = params;
|
|
325
|
+
const { requestId, action, actorContext, userText, channelDeliveryContext, emissionContext } = params;
|
|
322
326
|
|
|
323
327
|
// 1. Look up the canonical request
|
|
324
328
|
const request = getCanonicalGuardianRequest(requestId);
|
|
@@ -426,6 +430,7 @@ export async function applyCanonicalGuardianDecision(
|
|
|
426
430
|
// 5. Dispatch to kind-specific resolver
|
|
427
431
|
let resolverFailed = false;
|
|
428
432
|
let resolverFailureReason: string | undefined;
|
|
433
|
+
let resolverReplyText: string | undefined;
|
|
429
434
|
const resolver = getResolver(request.kind);
|
|
430
435
|
if (resolver) {
|
|
431
436
|
const resolverResult = await resolver.resolve({
|
|
@@ -433,6 +438,7 @@ export async function applyCanonicalGuardianDecision(
|
|
|
433
438
|
decision: { action: effectiveAction, userText },
|
|
434
439
|
actor: actorContext,
|
|
435
440
|
channelDeliveryContext,
|
|
441
|
+
emissionContext,
|
|
436
442
|
});
|
|
437
443
|
|
|
438
444
|
if (!resolverResult.ok) {
|
|
@@ -451,6 +457,8 @@ export async function applyCanonicalGuardianDecision(
|
|
|
451
457
|
// still being informed that the resolver had an issue.
|
|
452
458
|
resolverFailed = true;
|
|
453
459
|
resolverFailureReason = resolverResult.reason;
|
|
460
|
+
} else {
|
|
461
|
+
resolverReplyText = resolverResult.guardianReplyText;
|
|
454
462
|
}
|
|
455
463
|
} else {
|
|
456
464
|
log.info(
|
|
@@ -493,5 +501,6 @@ export async function applyCanonicalGuardianDecision(
|
|
|
493
501
|
requestId,
|
|
494
502
|
grantMinted,
|
|
495
503
|
...(resolverFailed ? { resolverFailed, resolverFailureReason } : {}),
|
|
504
|
+
...(resolverReplyText ? { resolverReplyText } : {}),
|
|
496
505
|
};
|
|
497
506
|
}
|
|
@@ -12,18 +12,27 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { answerCall } from '../calls/call-domain.js';
|
|
15
|
-
import
|
|
15
|
+
import { getGatewayInternalBaseUrl } from '../config/env.js';
|
|
16
|
+
import { type CanonicalGuardianRequest,getCanonicalGuardianRequest } from '../memory/canonical-guardian-store.js';
|
|
17
|
+
import { upsertMember } from '../memory/ingress-member-store.js';
|
|
16
18
|
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
17
19
|
import { addRule } from '../permissions/trust-store.js';
|
|
20
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
18
21
|
import type { ApprovalAction } from '../runtime/channel-approval-types.js';
|
|
19
22
|
import { createOutboundSession } from '../runtime/channel-guardian-service.js';
|
|
20
23
|
import { deliverChannelReply } from '../runtime/gateway-client.js';
|
|
21
24
|
import * as pendingInteractions from '../runtime/pending-interactions.js';
|
|
22
25
|
import { getTool } from '../tools/registry.js';
|
|
26
|
+
import { TC_GRANT_WAIT_MAX_MS } from '../tools/tool-approval-handler.js';
|
|
23
27
|
import { getLogger } from '../util/logger.js';
|
|
28
|
+
import { readHttpToken } from '../util/platform.js';
|
|
24
29
|
|
|
25
30
|
const log = getLogger('guardian-request-resolvers');
|
|
26
31
|
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
27
36
|
// ---------------------------------------------------------------------------
|
|
28
37
|
// Types
|
|
29
38
|
// ---------------------------------------------------------------------------
|
|
@@ -58,6 +67,13 @@ export interface ChannelDeliveryContext {
|
|
|
58
67
|
bearerToken?: string;
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
/** Emission context threaded from callers to handleConfirmationResponse. */
|
|
71
|
+
export interface ResolverEmissionContext {
|
|
72
|
+
source?: 'button' | 'inline_nl' | 'auto_deny' | 'timeout' | 'system';
|
|
73
|
+
causedByRequestId?: string;
|
|
74
|
+
decisionText?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
/** Context passed to each resolver after CAS resolution succeeds. */
|
|
62
78
|
export interface ResolverContext {
|
|
63
79
|
/** The canonical request record (already resolved to its terminal status). */
|
|
@@ -68,13 +84,27 @@ export interface ResolverContext {
|
|
|
68
84
|
actor: ActorContext;
|
|
69
85
|
/** Optional channel delivery context — present when the decision arrived via a channel message. */
|
|
70
86
|
channelDeliveryContext?: ChannelDeliveryContext;
|
|
87
|
+
/** Optional emission context threaded to handleConfirmationResponse for correct source attribution. */
|
|
88
|
+
emissionContext?: ResolverEmissionContext;
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
/** Discriminated result from a resolver. */
|
|
74
92
|
export type ResolverResult =
|
|
75
|
-
| { ok: true; applied: true; grantMinted?: boolean }
|
|
93
|
+
| { ok: true; applied: true; grantMinted?: boolean; guardianReplyText?: string }
|
|
76
94
|
| { ok: false; reason: string };
|
|
77
95
|
|
|
96
|
+
function resolveDeliverCallbackUrlForChannel(channel: string): string | null {
|
|
97
|
+
switch (channel) {
|
|
98
|
+
case 'telegram':
|
|
99
|
+
case 'sms':
|
|
100
|
+
case 'whatsapp':
|
|
101
|
+
case 'slack':
|
|
102
|
+
return `${getGatewayInternalBaseUrl()}/deliver/${channel}`;
|
|
103
|
+
default:
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
/** Interface that kind-specific resolvers implement. */
|
|
79
109
|
export interface GuardianRequestResolver {
|
|
80
110
|
/** The request kind this resolver handles (matches canonical_guardian_requests.kind). */
|
|
@@ -133,11 +163,13 @@ const pendingInteractionResolver: GuardianRequestResolver = {
|
|
|
133
163
|
decision.action === 'approve_always' &&
|
|
134
164
|
details &&
|
|
135
165
|
details.persistentDecisionsAllowed !== false &&
|
|
136
|
-
details.allowlistOptions?.length
|
|
137
|
-
details.scopeOptions?.length
|
|
166
|
+
details.allowlistOptions?.length
|
|
138
167
|
) {
|
|
139
168
|
const pattern = details.allowlistOptions[0].pattern;
|
|
140
|
-
|
|
169
|
+
// Non-scoped tools (web_fetch, network_request, etc.) have empty
|
|
170
|
+
// scopeOptions — default to 'everywhere' so approve_always still
|
|
171
|
+
// persists a trust rule instead of silently degrading to one-time.
|
|
172
|
+
const scope = details.scopeOptions?.length ? details.scopeOptions[0].scope : 'everywhere';
|
|
141
173
|
const tool = getTool(details.toolName);
|
|
142
174
|
const executionTarget = tool?.origin === 'skill' ? details.executionTarget : undefined;
|
|
143
175
|
addRule(details.toolName, pattern, scope, 'allow', 100, { executionTarget });
|
|
@@ -160,7 +192,7 @@ const pendingInteractionResolver: GuardianRequestResolver = {
|
|
|
160
192
|
|
|
161
193
|
// Map action to the permission system's UserDecision type and notify session.
|
|
162
194
|
const userDecision = decision.action === 'reject' ? 'deny' as const : 'allow' as const;
|
|
163
|
-
resolved.session.handleConfirmationResponse(request.id, userDecision);
|
|
195
|
+
resolved.session.handleConfirmationResponse(request.id, userDecision, undefined, undefined, undefined, ctx.emissionContext);
|
|
164
196
|
|
|
165
197
|
log.info(
|
|
166
198
|
{
|
|
@@ -277,8 +309,11 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
277
309
|
const channel = request.sourceChannel ?? 'unknown';
|
|
278
310
|
const requesterExternalUserId = request.requesterExternalUserId ?? '';
|
|
279
311
|
const requesterChatId = request.requesterChatId ?? request.requesterExternalUserId ?? '';
|
|
312
|
+
const requesterLabel = requesterExternalUserId || requesterChatId || 'the requester';
|
|
280
313
|
const decidedByExternalUserId = ctx.actor.externalUserId ?? '';
|
|
281
|
-
const assistantId =
|
|
314
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
315
|
+
const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
|
|
316
|
+
const desktopBearerToken = readHttpToken() ?? undefined;
|
|
282
317
|
|
|
283
318
|
if (decision.action === 'reject') {
|
|
284
319
|
log.info(
|
|
@@ -335,12 +370,59 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
335
370
|
contextPayload: deniedPayload,
|
|
336
371
|
dedupeKey: `trusted-contact:denied:${request.id}`,
|
|
337
372
|
});
|
|
373
|
+
} else if (desktopDeliverUrl && requesterChatId) {
|
|
374
|
+
try {
|
|
375
|
+
await deliverChannelReply(desktopDeliverUrl, {
|
|
376
|
+
chatId: requesterChatId,
|
|
377
|
+
text: 'Your access request has been denied by the guardian.',
|
|
378
|
+
assistantId,
|
|
379
|
+
}, desktopBearerToken);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
log.error({ err, requesterChatId }, 'Failed to notify requester of access request denial (desktop decision path)');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
ok: true,
|
|
387
|
+
applied: true,
|
|
388
|
+
...(ctx.actor.isTrusted ? { guardianReplyText: `Access denied for ${requesterLabel}.` } : {}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Voice approvals: directly activate the trusted contact without minting
|
|
393
|
+
// a verification session. The caller is already on the line and the
|
|
394
|
+
// relay server's in-call wait loop will detect the approved status.
|
|
395
|
+
if (channel === 'voice') {
|
|
396
|
+
try {
|
|
397
|
+
upsertMember({
|
|
398
|
+
assistantId,
|
|
399
|
+
sourceChannel: 'voice',
|
|
400
|
+
externalUserId: requesterExternalUserId,
|
|
401
|
+
externalChatId: requesterChatId,
|
|
402
|
+
status: 'active',
|
|
403
|
+
policy: 'allow',
|
|
404
|
+
});
|
|
405
|
+
} catch (err) {
|
|
406
|
+
log.error(
|
|
407
|
+
{ err, requesterExternalUserId },
|
|
408
|
+
'Access request resolver: failed to activate voice caller as trusted contact',
|
|
409
|
+
);
|
|
338
410
|
}
|
|
339
411
|
|
|
412
|
+
log.info(
|
|
413
|
+
{
|
|
414
|
+
event: 'resolver_access_request_voice_approved',
|
|
415
|
+
requestId: request.id,
|
|
416
|
+
channel,
|
|
417
|
+
requesterExternalUserId,
|
|
418
|
+
},
|
|
419
|
+
'Access request resolver: voice approval — direct trusted-contact activation (no verification session)',
|
|
420
|
+
);
|
|
421
|
+
|
|
340
422
|
return { ok: true, applied: true };
|
|
341
423
|
}
|
|
342
424
|
|
|
343
|
-
//
|
|
425
|
+
// Non-voice approvals: mint an identity-bound verification session so the
|
|
344
426
|
// requester can verify their identity.
|
|
345
427
|
const session = createOutboundSession({
|
|
346
428
|
assistantId,
|
|
@@ -365,6 +447,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
365
447
|
|
|
366
448
|
// Deliver the verification code to the guardian and notify the requester
|
|
367
449
|
// when channel delivery context is available (channel message path).
|
|
450
|
+
let requesterNotified = false;
|
|
368
451
|
if (channelDeliveryContext) {
|
|
369
452
|
let codeDelivered = true;
|
|
370
453
|
|
|
@@ -395,6 +478,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
395
478
|
+ 'Please enter the 6-digit verification code you receive from the guardian.',
|
|
396
479
|
assistantId,
|
|
397
480
|
}, channelDeliveryContext.bearerToken);
|
|
481
|
+
requesterNotified = true;
|
|
398
482
|
} catch (err) {
|
|
399
483
|
log.error({ err, requesterChatId }, 'Failed to notify requester of access request approval');
|
|
400
484
|
}
|
|
@@ -434,9 +518,29 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
434
518
|
dedupeKey: `trusted-contact:verification-sent:${session.sessionId}`,
|
|
435
519
|
});
|
|
436
520
|
}
|
|
521
|
+
} else if (desktopDeliverUrl && requesterChatId) {
|
|
522
|
+
try {
|
|
523
|
+
await deliverChannelReply(desktopDeliverUrl, {
|
|
524
|
+
chatId: requesterChatId,
|
|
525
|
+
text: 'Your access request has been approved! '
|
|
526
|
+
+ 'Please enter the 6-digit verification code you receive from the guardian.',
|
|
527
|
+
assistantId,
|
|
528
|
+
}, desktopBearerToken);
|
|
529
|
+
requesterNotified = true;
|
|
530
|
+
} catch (err) {
|
|
531
|
+
log.error({ err, requesterChatId }, 'Failed to notify requester of access request approval (desktop decision path)');
|
|
532
|
+
}
|
|
437
533
|
}
|
|
438
534
|
|
|
439
|
-
|
|
535
|
+
const verificationReplyText = requesterNotified
|
|
536
|
+
? `Access approved for ${requesterLabel}. Give them this verification code: ${session.secret}. The code expires in 10 minutes.`
|
|
537
|
+
: `Access approved for ${requesterLabel}. Give them this verification code: ${session.secret}. The code expires in 10 minutes. I could not notify them automatically, so please tell them to send the code manually.`;
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
ok: true,
|
|
541
|
+
applied: true,
|
|
542
|
+
...(ctx.actor.isTrusted ? { guardianReplyText: verificationReplyText } : {}),
|
|
543
|
+
};
|
|
440
544
|
},
|
|
441
545
|
};
|
|
442
546
|
|
|
@@ -461,7 +565,7 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
|
|
|
461
565
|
async resolve(ctx: ResolverContext): Promise<ResolverResult> {
|
|
462
566
|
const { request, decision, channelDeliveryContext } = ctx;
|
|
463
567
|
const requesterChatId = request.requesterChatId ?? request.requesterExternalUserId ?? '';
|
|
464
|
-
const assistantId =
|
|
568
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
465
569
|
|
|
466
570
|
if (decision.action === 'reject') {
|
|
467
571
|
log.info(
|
|
@@ -495,7 +599,61 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
|
|
|
495
599
|
'Tool grant request resolver: approved (grant minting deferred to canonical primitive)',
|
|
496
600
|
);
|
|
497
601
|
|
|
498
|
-
|
|
602
|
+
// Re-read the canonical request to check whether an inline grant waiter
|
|
603
|
+
// has already claimed this request. When followupState is
|
|
604
|
+
// 'inline_wait_active', the requester's original tool call is blocking
|
|
605
|
+
// on the grant and will resume automatically — sending a "please retry"
|
|
606
|
+
// notification would be stale and confusing (and could cause duplicate
|
|
607
|
+
// attempts or one-time-grant denials).
|
|
608
|
+
//
|
|
609
|
+
// Staleness guard: the inline_wait_active marker is persisted in DB and
|
|
610
|
+
// can outlive the actual waiter if the daemon crashes or restarts during
|
|
611
|
+
// the wait. To avoid permanently suppressing the retry notification, we
|
|
612
|
+
// treat the marker as stale if the encoded start timestamp is older than
|
|
613
|
+
// the maximum wait budget plus a 30s buffer.
|
|
614
|
+
const INLINE_WAIT_STALENESS_BUFFER_MS = 30_000;
|
|
615
|
+
const freshRequest = getCanonicalGuardianRequest(request.id);
|
|
616
|
+
const followupState = freshRequest?.followupState ?? '';
|
|
617
|
+
let inlineWaitActive = followupState.startsWith('inline_wait_active');
|
|
618
|
+
if (inlineWaitActive && freshRequest) {
|
|
619
|
+
// The followupState encodes the wall-clock epoch when the inline wait
|
|
620
|
+
// started (e.g. 'inline_wait_active:1700000000000'). We use this
|
|
621
|
+
// instead of updatedAt because resolveCanonicalGuardianRequest sets
|
|
622
|
+
// updatedAt = now during CAS resolution, making updatedAt always fresh
|
|
623
|
+
// by the time this resolver runs.
|
|
624
|
+
const colonIdx = followupState.indexOf(':');
|
|
625
|
+
const waitStartMs = colonIdx !== -1 ? Number(followupState.slice(colonIdx + 1)) : NaN;
|
|
626
|
+
const markerAgeMs = Number.isFinite(waitStartMs)
|
|
627
|
+
? Date.now() - waitStartMs
|
|
628
|
+
: Infinity; // Treat unparseable timestamps as stale for safety.
|
|
629
|
+
const stalenessThresholdMs = TC_GRANT_WAIT_MAX_MS + INLINE_WAIT_STALENESS_BUFFER_MS;
|
|
630
|
+
if (markerAgeMs > stalenessThresholdMs) {
|
|
631
|
+
log.warn(
|
|
632
|
+
{
|
|
633
|
+
event: 'resolver_tool_grant_request_stale_inline_wait',
|
|
634
|
+
requestId: request.id,
|
|
635
|
+
toolName: request.toolName,
|
|
636
|
+
markerAgeMs,
|
|
637
|
+
stalenessThresholdMs,
|
|
638
|
+
waitStartMs,
|
|
639
|
+
},
|
|
640
|
+
'inline_wait_active marker is stale (daemon likely crashed during wait) — sending retry notification',
|
|
641
|
+
);
|
|
642
|
+
inlineWaitActive = false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (inlineWaitActive) {
|
|
647
|
+
log.info(
|
|
648
|
+
{
|
|
649
|
+
event: 'resolver_tool_grant_request_skip_retry_notification',
|
|
650
|
+
requestId: request.id,
|
|
651
|
+
toolName: request.toolName,
|
|
652
|
+
followupState: freshRequest?.followupState,
|
|
653
|
+
},
|
|
654
|
+
'Skipping requester retry notification — inline grant wait is active and will resume the original invocation',
|
|
655
|
+
);
|
|
656
|
+
} else if (channelDeliveryContext && requesterChatId) {
|
|
499
657
|
try {
|
|
500
658
|
await deliverChannelReply(channelDeliveryContext.replyCallbackUrl, {
|
|
501
659
|
chatId: requesterChatId,
|
|
@@ -41,6 +41,35 @@ export function getUserConsultationTimeoutMs(): number {
|
|
|
41
41
|
return getConfig().calls.userConsultTimeoutSeconds * 1000;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export function getTtsPlaybackDelayMs(): number {
|
|
45
|
+
return getConfig().calls.ttsPlaybackDelayMs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getAccessRequestPollIntervalMs(): number {
|
|
49
|
+
return getConfig().calls.accessRequestPollIntervalMs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getGuardianWaitUpdateInitialIntervalMs(): number {
|
|
53
|
+
return getConfig().calls.guardianWaitUpdateInitialIntervalMs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getGuardianWaitUpdateInitialWindowMs(): number {
|
|
57
|
+
return getConfig().calls.guardianWaitUpdateInitialWindowMs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getGuardianWaitUpdateSteadyMinIntervalMs(): number {
|
|
61
|
+
return getConfig().calls.guardianWaitUpdateSteadyMinIntervalMs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getGuardianWaitUpdateSteadyMaxIntervalMs(): number {
|
|
65
|
+
return getConfig().calls.guardianWaitUpdateSteadyMaxIntervalMs;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getSilenceTimeoutMs(): number {
|
|
69
|
+
return 30 * 1000; // 30 seconds
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @deprecated Use getSilenceTimeoutMs() for mockability in tests. */
|
|
44
73
|
export const SILENCE_TIMEOUT_MS = 30 * 1000; // 30 seconds
|
|
45
74
|
|
|
46
75
|
// Legacy named exports for backward compatibility (use functions above for config-backed values)
|
|
@@ -18,10 +18,11 @@ import {
|
|
|
18
18
|
listCanonicalGuardianDeliveries,
|
|
19
19
|
} from '../memory/canonical-guardian-store.js';
|
|
20
20
|
import { revokeScopedApprovalGrantsForContext } from '../memory/scoped-approval-grants.js';
|
|
21
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
21
22
|
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
22
23
|
import { getLogger } from '../util/logger.js';
|
|
23
24
|
import { readHttpToken } from '../util/platform.js';
|
|
24
|
-
import { getMaxCallDurationMs,
|
|
25
|
+
import { getMaxCallDurationMs, getSilenceTimeoutMs, getUserConsultationTimeoutMs } from './call-constants.js';
|
|
25
26
|
import { persistCallCompletionMessage } from './call-conversation-messages.js';
|
|
26
27
|
import { addPointerMessage, formatDuration } from './call-pointer-messages.js';
|
|
27
28
|
import { fireCallCompletionNotifier, fireCallQuestionNotifier, fireCallTranscriptNotifier,registerCallController, unregisterCallController } from './call-state.js';
|
|
@@ -245,7 +246,7 @@ export class CallController {
|
|
|
245
246
|
this.task = task;
|
|
246
247
|
this.isInbound = !task;
|
|
247
248
|
this.broadcast = opts?.broadcast;
|
|
248
|
-
this.assistantId = opts?.assistantId ??
|
|
249
|
+
this.assistantId = opts?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
249
250
|
this.guardianContext = opts?.guardianContext ?? null;
|
|
250
251
|
|
|
251
252
|
// Resolve the conversation ID from the call session
|
|
@@ -1048,8 +1049,15 @@ export class CallController {
|
|
|
1048
1049
|
private resetSilenceTimer(): void {
|
|
1049
1050
|
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
1050
1051
|
this.silenceTimer = setTimeout(() => {
|
|
1052
|
+
// During guardian wait states, the relay heartbeat timer handles
|
|
1053
|
+
// periodic updates — suppress the generic "Are you still there?"
|
|
1054
|
+
// which is confusing when the caller is waiting on a decision.
|
|
1055
|
+
if (this.relay.getConnectionState() === 'awaiting_guardian_decision') {
|
|
1056
|
+
log.debug({ callSessionId: this.callSessionId }, 'Silence timeout suppressed during guardian wait');
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1051
1059
|
log.info({ callSessionId: this.callSessionId }, 'Silence timeout triggered');
|
|
1052
1060
|
this.relay.sendTextToken('Are you still there?', true);
|
|
1053
|
-
},
|
|
1061
|
+
}, getSilenceTimeoutMs());
|
|
1054
1062
|
}
|
|
1055
1063
|
}
|