@vellumai/assistant 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/ARCHITECTURE.md +40 -3
- package/README.md +43 -35
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -87
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +4 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -4
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +131 -8
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +62 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +841 -39
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +10 -2
- package/src/approvals/guardian-request-resolvers.ts +128 -9
- package/src/calls/call-constants.ts +21 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-domain.ts +28 -7
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +424 -12
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
- package/src/config/calls-schema.ts +24 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +5 -55
- package/src/daemon/handlers/config-inbox.ts +9 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/sessions.ts +48 -3
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/integrations.ts +1 -99
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +14 -1
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +22 -11
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +3 -0
- package/src/daemon/session-surfaces.ts +3 -2
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +16 -0
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +39 -1
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +16 -8
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +71 -1
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +0 -3
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +65 -12
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/invite-redemption-service.ts +8 -0
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/conversation-routes.ts +140 -52
- package/src/runtime/routes/events-routes.ts +20 -5
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-message-handler.ts +143 -2
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/tool-approval-handler.ts +242 -18
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
handleGetAttachmentContent,
|
|
88
88
|
handleUploadAttachment,
|
|
89
89
|
} from './routes/attachment-routes.js';
|
|
90
|
+
import { handleGetBrainGraph, handleServeBrainGraphUI, handleServeHomeBaseUI } from './routes/brain-graph-routes.js';
|
|
90
91
|
import {
|
|
91
92
|
handleAnswerCall,
|
|
92
93
|
handleCancelCall,
|
|
@@ -98,6 +99,10 @@ import {
|
|
|
98
99
|
startCanonicalGuardianExpirySweep,
|
|
99
100
|
stopCanonicalGuardianExpirySweep,
|
|
100
101
|
} from './routes/canonical-guardian-expiry-sweep.js';
|
|
102
|
+
import {
|
|
103
|
+
handleGetChannelReadiness,
|
|
104
|
+
handleRefreshChannelReadiness,
|
|
105
|
+
} from './routes/channel-readiness-routes.js';
|
|
101
106
|
import {
|
|
102
107
|
handleChannelDeliveryAck,
|
|
103
108
|
handleChannelInbound,
|
|
@@ -126,6 +131,7 @@ import {
|
|
|
126
131
|
handleGuardianActionDecision,
|
|
127
132
|
handleGuardianActionsPending,
|
|
128
133
|
} from './routes/guardian-action-routes.js';
|
|
134
|
+
import { handleGuardianBootstrap } from './routes/guardian-bootstrap-routes.js';
|
|
129
135
|
import { handleGetIdentity,handleHealth } from './routes/identity-routes.js';
|
|
130
136
|
import {
|
|
131
137
|
handleBlockMember,
|
|
@@ -160,6 +166,21 @@ import {
|
|
|
160
166
|
handlePairingStatus,
|
|
161
167
|
} from './routes/pairing-routes.js';
|
|
162
168
|
import { handleAddSecret } from './routes/secret-routes.js';
|
|
169
|
+
import {
|
|
170
|
+
handleAssignTwilioNumber,
|
|
171
|
+
handleClearTwilioCredentials,
|
|
172
|
+
handleDeleteTollfreeVerification,
|
|
173
|
+
handleGetSmsCompliance,
|
|
174
|
+
handleGetTwilioConfig,
|
|
175
|
+
handleListTwilioNumbers,
|
|
176
|
+
handleProvisionTwilioNumber,
|
|
177
|
+
handleReleaseTwilioNumber,
|
|
178
|
+
handleSetTwilioCredentials,
|
|
179
|
+
handleSmsDoctor,
|
|
180
|
+
handleSmsSendTest,
|
|
181
|
+
handleSubmitTollfreeVerification,
|
|
182
|
+
handleUpdateTollfreeVerification,
|
|
183
|
+
} from './routes/twilio-routes.js';
|
|
163
184
|
|
|
164
185
|
// Re-export for consumers
|
|
165
186
|
export { isPrivateAddress } from './middleware/auth.js';
|
|
@@ -445,7 +466,7 @@ export class RuntimeHttpServer {
|
|
|
445
466
|
return rateLimitResponse(result);
|
|
446
467
|
}
|
|
447
468
|
// Attach rate limit headers to the eventual response
|
|
448
|
-
const originalResponse = await this.handleAuthenticatedRequest(req, url, path);
|
|
469
|
+
const originalResponse = await this.handleAuthenticatedRequest(req, url, path, server);
|
|
449
470
|
const headers = new Headers(originalResponse.headers);
|
|
450
471
|
for (const [k, v] of Object.entries(rateLimitHeaders(result))) {
|
|
451
472
|
headers.set(k, v);
|
|
@@ -457,13 +478,13 @@ export class RuntimeHttpServer {
|
|
|
457
478
|
});
|
|
458
479
|
}
|
|
459
480
|
|
|
460
|
-
return this.handleAuthenticatedRequest(req, url, path);
|
|
481
|
+
return this.handleAuthenticatedRequest(req, url, path, server);
|
|
461
482
|
}
|
|
462
483
|
|
|
463
484
|
/**
|
|
464
485
|
* Handle requests that have already passed auth and rate limiting.
|
|
465
486
|
*/
|
|
466
|
-
private async handleAuthenticatedRequest(req: Request, url: URL, path: string): Promise<Response> {
|
|
487
|
+
private async handleAuthenticatedRequest(req: Request, url: URL, path: string, server: ReturnType<typeof Bun.serve>): Promise<Response> {
|
|
467
488
|
// Pairing registration (bearer-authenticated)
|
|
468
489
|
if (path === '/v1/pairing/register' && req.method === 'POST') {
|
|
469
490
|
return await handlePairingRegister(req, this.pairingContext);
|
|
@@ -524,7 +545,7 @@ export class RuntimeHttpServer {
|
|
|
524
545
|
// Runtime routes: /v1/<endpoint>
|
|
525
546
|
const routeMatch = path.match(/^\/v1\/(.+)$/);
|
|
526
547
|
if (routeMatch) {
|
|
527
|
-
return this.dispatchEndpoint(routeMatch[1], req, url);
|
|
548
|
+
return this.dispatchEndpoint(routeMatch[1], req, url, server);
|
|
528
549
|
}
|
|
529
550
|
|
|
530
551
|
return httpError('NOT_FOUND', 'Not found', 404);
|
|
@@ -607,6 +628,7 @@ export class RuntimeHttpServer {
|
|
|
607
628
|
endpoint: string,
|
|
608
629
|
req: Request,
|
|
609
630
|
url: URL,
|
|
631
|
+
server: ReturnType<typeof Bun.serve>,
|
|
610
632
|
): Promise<Response> {
|
|
611
633
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
612
634
|
return withErrorHandling(endpoint, async () => {
|
|
@@ -706,18 +728,18 @@ export class RuntimeHttpServer {
|
|
|
706
728
|
persistAndProcessMessage: this.persistAndProcessMessage,
|
|
707
729
|
sendMessageDeps: this.sendMessageDeps,
|
|
708
730
|
approvalConversationGenerator: this.approvalConversationGenerator,
|
|
709
|
-
});
|
|
731
|
+
}, server);
|
|
710
732
|
}
|
|
711
733
|
|
|
712
734
|
// Standalone approval endpoints — keyed by requestId, orthogonal to message sending
|
|
713
|
-
if (endpoint === 'confirm' && req.method === 'POST') return await handleConfirm(req);
|
|
714
|
-
if (endpoint === 'secret' && req.method === 'POST') return await handleSecret(req);
|
|
715
|
-
if (endpoint === 'trust-rules' && req.method === 'POST') return await handleTrustRule(req);
|
|
716
|
-
if (endpoint === 'pending-interactions' && req.method === 'GET') return handleListPendingInteractions(url);
|
|
735
|
+
if (endpoint === 'confirm' && req.method === 'POST') return await handleConfirm(req, server);
|
|
736
|
+
if (endpoint === 'secret' && req.method === 'POST') return await handleSecret(req, server);
|
|
737
|
+
if (endpoint === 'trust-rules' && req.method === 'POST') return await handleTrustRule(req, server);
|
|
738
|
+
if (endpoint === 'pending-interactions' && req.method === 'GET') return handleListPendingInteractions(url, req, server);
|
|
717
739
|
|
|
718
740
|
// Guardian action endpoints — deterministic button-based decisions
|
|
719
|
-
if (endpoint === 'guardian-actions/pending' && req.method === 'GET') return handleGuardianActionsPending(req);
|
|
720
|
-
if (endpoint === 'guardian-actions/decision' && req.method === 'POST') return await handleGuardianActionDecision(req);
|
|
741
|
+
if (endpoint === 'guardian-actions/pending' && req.method === 'GET') return handleGuardianActionsPending(req, server);
|
|
742
|
+
if (endpoint === 'guardian-actions/decision' && req.method === 'POST') return await handleGuardianActionDecision(req, server);
|
|
721
743
|
|
|
722
744
|
// Contacts
|
|
723
745
|
if (endpoint === 'contacts' && req.method === 'GET') return handleListContacts(url);
|
|
@@ -759,6 +781,34 @@ export class RuntimeHttpServer {
|
|
|
759
781
|
if (endpoint === 'integrations/guardian/outbound/resend' && req.method === 'POST') return await handleResendOutbound(req);
|
|
760
782
|
if (endpoint === 'integrations/guardian/outbound/cancel' && req.method === 'POST') return await handleCancelOutbound(req);
|
|
761
783
|
|
|
784
|
+
// Guardian vellum channel bootstrap
|
|
785
|
+
if (endpoint === 'integrations/guardian/vellum/bootstrap' && req.method === 'POST') return await handleGuardianBootstrap(req, server);
|
|
786
|
+
|
|
787
|
+
// Integrations — Twilio config
|
|
788
|
+
if (endpoint === 'integrations/twilio/config' && req.method === 'GET') return handleGetTwilioConfig();
|
|
789
|
+
if (endpoint === 'integrations/twilio/credentials' && req.method === 'POST') return await handleSetTwilioCredentials(req);
|
|
790
|
+
if (endpoint === 'integrations/twilio/credentials' && req.method === 'DELETE') return handleClearTwilioCredentials();
|
|
791
|
+
if (endpoint === 'integrations/twilio/numbers' && req.method === 'GET') return await handleListTwilioNumbers();
|
|
792
|
+
if (endpoint === 'integrations/twilio/numbers/provision' && req.method === 'POST') return await handleProvisionTwilioNumber(req);
|
|
793
|
+
if (endpoint === 'integrations/twilio/numbers/assign' && req.method === 'POST') return await handleAssignTwilioNumber(req);
|
|
794
|
+
if (endpoint === 'integrations/twilio/numbers/release' && req.method === 'POST') return await handleReleaseTwilioNumber(req);
|
|
795
|
+
if (endpoint === 'integrations/twilio/sms/compliance' && req.method === 'GET') return await handleGetSmsCompliance();
|
|
796
|
+
if (endpoint === 'integrations/twilio/sms/compliance/tollfree' && req.method === 'POST') return await handleSubmitTollfreeVerification(req);
|
|
797
|
+
if (endpoint === 'integrations/twilio/sms/test' && req.method === 'POST') return await handleSmsSendTest(req);
|
|
798
|
+
if (endpoint === 'integrations/twilio/sms/doctor' && req.method === 'POST') return await handleSmsDoctor();
|
|
799
|
+
|
|
800
|
+
// Twilio toll-free verification PATCH/DELETE with :verificationSid
|
|
801
|
+
const tollfreeVerificationMatch = endpoint.match(/^integrations\/twilio\/sms\/compliance\/tollfree\/([^/]+)$/);
|
|
802
|
+
if (tollfreeVerificationMatch) {
|
|
803
|
+
const verificationSid = tollfreeVerificationMatch[1];
|
|
804
|
+
if (req.method === 'PATCH') return await handleUpdateTollfreeVerification(req, verificationSid);
|
|
805
|
+
if (req.method === 'DELETE') return await handleDeleteTollfreeVerification(verificationSid);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Channel readiness
|
|
809
|
+
if (endpoint === 'channels/readiness' && req.method === 'GET') return await handleGetChannelReadiness(url);
|
|
810
|
+
if (endpoint === 'channels/readiness/refresh' && req.method === 'POST') return await handleRefreshChannelReadiness(req);
|
|
811
|
+
|
|
762
812
|
if (endpoint === 'attachments' && req.method === 'POST') return await handleUploadAttachment(req);
|
|
763
813
|
if (endpoint === 'attachments' && req.method === 'DELETE') return await handleDeleteAttachment(req);
|
|
764
814
|
|
|
@@ -826,7 +876,10 @@ export class RuntimeHttpServer {
|
|
|
826
876
|
}
|
|
827
877
|
|
|
828
878
|
if (endpoint === 'identity' && req.method === 'GET') return handleGetIdentity();
|
|
829
|
-
if (endpoint === '
|
|
879
|
+
if (endpoint === 'brain-graph' && req.method === 'GET') return handleGetBrainGraph();
|
|
880
|
+
if (endpoint === 'brain-graph-ui' && req.method === 'GET') return handleServeBrainGraphUI(this.bearerToken);
|
|
881
|
+
if (endpoint === 'home-base-ui' && req.method === 'GET') return handleServeHomeBaseUI(this.bearerToken);
|
|
882
|
+
if (endpoint === 'events' && req.method === 'GET') return handleSubscribeAssistantEvents(req, url, { server });
|
|
830
883
|
|
|
831
884
|
// Internal OAuth callback endpoint (gateway -> runtime)
|
|
832
885
|
if (endpoint === 'internal/oauth/callback' && req.method === 'POST') {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types for the runtime HTTP server and its route handlers.
|
|
3
3
|
*/
|
|
4
|
+
import type {
|
|
5
|
+
CallPointerMessageContext,
|
|
6
|
+
ComposeCallPointerMessageOptions,
|
|
7
|
+
} from '../calls/call-pointer-message-composer.js';
|
|
4
8
|
import type { ChannelId, InterfaceId } from '../channels/types.js';
|
|
5
9
|
import type { Session } from '../daemon/session.js';
|
|
6
10
|
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
@@ -55,6 +59,15 @@ export type ApprovalConversationGenerator = (
|
|
|
55
59
|
context: ApprovalConversationContext,
|
|
56
60
|
) => Promise<ApprovalConversationResult>;
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Daemon-injected function that generates call pointer copy using a provider.
|
|
64
|
+
* Returns generated text or `null` on failure (caller falls back to deterministic text).
|
|
65
|
+
*/
|
|
66
|
+
export type PointerCopyGenerator = (
|
|
67
|
+
context: CallPointerMessageContext,
|
|
68
|
+
options?: ComposeCallPointerMessageOptions,
|
|
69
|
+
) => Promise<string | null>;
|
|
70
|
+
|
|
58
71
|
/**
|
|
59
72
|
* Daemon-injected function that generates guardian action copy using a provider.
|
|
60
73
|
* Returns generated text or `null` on failure (caller falls back to deterministic text).
|
|
@@ -286,12 +286,20 @@ export function redeemVoiceInviteCode(params: {
|
|
|
286
286
|
const STALE_INVITE = Symbol('stale_invite');
|
|
287
287
|
let memberId: string | undefined;
|
|
288
288
|
|
|
289
|
+
// Reactivation should not overwrite a guardian-managed nickname (same
|
|
290
|
+
// protection as the token-based redemption path above).
|
|
291
|
+
const preservedDisplayName = existingMember?.displayName?.trim().length
|
|
292
|
+
? existingMember.displayName
|
|
293
|
+
: (invite.friendName ?? undefined);
|
|
294
|
+
|
|
289
295
|
try {
|
|
290
296
|
getSqlite().transaction(() => {
|
|
291
297
|
const member = upsertMember({
|
|
292
298
|
assistantId: invite.assistantId,
|
|
293
299
|
sourceChannel: 'voice',
|
|
294
300
|
externalUserId: callerExternalUserId,
|
|
301
|
+
externalChatId: callerExternalUserId,
|
|
302
|
+
displayName: preservedDisplayName,
|
|
295
303
|
status: 'active',
|
|
296
304
|
policy: 'allow',
|
|
297
305
|
inviteId: invite.id,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic local actor identity for IPC connections.
|
|
3
|
+
*
|
|
4
|
+
* IPC (Unix domain socket) connections come from the local macOS native app.
|
|
5
|
+
* No actor token is sent over the socket; instead, the daemon assigns a
|
|
6
|
+
* deterministic local actor identity server-side by looking up the vellum
|
|
7
|
+
* channel guardian binding.
|
|
8
|
+
*
|
|
9
|
+
* This routes IPC connections through the same `resolveGuardianContext`
|
|
10
|
+
* pathway used by HTTP channel ingress, producing equivalent
|
|
11
|
+
* guardian-context behavior for the vellum channel.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ChannelId } from '../channels/types.js';
|
|
15
|
+
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
16
|
+
import { getActiveBinding } from '../memory/guardian-bindings.js';
|
|
17
|
+
import { getLogger } from '../util/logger.js';
|
|
18
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
19
|
+
import {
|
|
20
|
+
resolveGuardianContext,
|
|
21
|
+
toGuardianRuntimeContext,
|
|
22
|
+
} from './guardian-context-resolver.js';
|
|
23
|
+
|
|
24
|
+
const log = getLogger('local-actor-identity');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the guardian runtime context for a local IPC connection.
|
|
28
|
+
*
|
|
29
|
+
* Looks up the vellum guardian binding to obtain the `guardianPrincipalId`,
|
|
30
|
+
* then passes it as the sender identity through `resolveGuardianContext` --
|
|
31
|
+
* the same pathway HTTP channel routes use. This ensures IPC and HTTP
|
|
32
|
+
* produce equivalent trust classification for the vellum channel.
|
|
33
|
+
*
|
|
34
|
+
* When no vellum guardian binding exists (e.g. fresh install before
|
|
35
|
+
* bootstrap), falls back to a minimal guardian context so the local
|
|
36
|
+
* user is not incorrectly denied.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveLocalIpcGuardianContext(
|
|
39
|
+
sourceChannel: ChannelId = 'vellum',
|
|
40
|
+
): GuardianRuntimeContext {
|
|
41
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
42
|
+
const binding = getActiveBinding(assistantId, 'vellum');
|
|
43
|
+
|
|
44
|
+
if (!binding) {
|
|
45
|
+
// No vellum binding yet (pre-bootstrap). The local user is
|
|
46
|
+
// inherently the guardian of their own machine, so produce a
|
|
47
|
+
// guardian context without a binding match. The trust resolver
|
|
48
|
+
// would classify this as 'unknown' due to no_binding, but for
|
|
49
|
+
// the local IPC case that is incorrect -- the local macOS user
|
|
50
|
+
// is always the guardian.
|
|
51
|
+
log.debug('No vellum guardian binding found; using fallback guardian context for IPC');
|
|
52
|
+
return {
|
|
53
|
+
sourceChannel,
|
|
54
|
+
trustClass: 'guardian',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const guardianPrincipalId = binding.guardianExternalUserId;
|
|
59
|
+
|
|
60
|
+
// Route through the shared trust resolution pipeline using 'vellum'
|
|
61
|
+
// as the channel for binding lookup. The guardianPrincipalId comes
|
|
62
|
+
// from the vellum binding, so the binding lookup must also target
|
|
63
|
+
// 'vellum' — otherwise resolveActorTrust would look up a different
|
|
64
|
+
// channel's binding (e.g. telegram/sms) and the IDs wouldn't match,
|
|
65
|
+
// causing a 'unknown' trust classification.
|
|
66
|
+
const guardianCtx = resolveGuardianContext({
|
|
67
|
+
assistantId,
|
|
68
|
+
sourceChannel: 'vellum',
|
|
69
|
+
externalChatId: 'local',
|
|
70
|
+
senderExternalUserId: guardianPrincipalId,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Overlay the caller's actual sourceChannel onto the resolved context
|
|
74
|
+
// so downstream consumers see the correct channel provenance.
|
|
75
|
+
return toGuardianRuntimeContext(sourceChannel, guardianCtx);
|
|
76
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actor-token verification middleware for HTTP routes.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the X-Actor-Token header, verifies the HMAC signature,
|
|
5
|
+
* checks that the token is active in the store, and returns the
|
|
6
|
+
* verified claims and resolved guardian runtime context.
|
|
7
|
+
*
|
|
8
|
+
* Used by vellum-channel HTTP routes (POST /v1/messages, POST /v1/confirm,
|
|
9
|
+
* POST /v1/guardian-actions/decision, etc.) to enforce identity-based
|
|
10
|
+
* authentication.
|
|
11
|
+
*
|
|
12
|
+
* For backward compatibility with bearer-authenticated local clients (CLI),
|
|
13
|
+
* provides fallback functions that resolve identity through the local IPC
|
|
14
|
+
* guardian context pathway when no actor token is present.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ChannelId } from '../../channels/types.js';
|
|
18
|
+
import type { GuardianRuntimeContext } from '../../daemon/session-runtime-assembly.js';
|
|
19
|
+
import { getActiveBinding } from '../../memory/guardian-bindings.js';
|
|
20
|
+
import { getLogger } from '../../util/logger.js';
|
|
21
|
+
import { type ActorTokenClaims, hashToken, verifyActorToken } from '../actor-token-service.js';
|
|
22
|
+
import { findActiveByTokenHash } from '../actor-token-store.js';
|
|
23
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
24
|
+
import {
|
|
25
|
+
resolveGuardianContext,
|
|
26
|
+
toGuardianRuntimeContext,
|
|
27
|
+
} from '../guardian-context-resolver.js';
|
|
28
|
+
import { resolveLocalIpcGuardianContext } from '../local-actor-identity.js';
|
|
29
|
+
|
|
30
|
+
const log = getLogger('actor-token-middleware');
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Types
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface ActorTokenResult {
|
|
37
|
+
ok: true;
|
|
38
|
+
claims: ActorTokenClaims;
|
|
39
|
+
guardianContext: GuardianRuntimeContext;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ActorTokenError {
|
|
43
|
+
ok: false;
|
|
44
|
+
status: number;
|
|
45
|
+
message: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ActorTokenVerification = ActorTokenResult | ActorTokenError;
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Header extraction
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
const ACTOR_TOKEN_HEADER = 'x-actor-token';
|
|
55
|
+
|
|
56
|
+
export function extractActorToken(req: Request): string | null {
|
|
57
|
+
return req.headers.get(ACTOR_TOKEN_HEADER) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Full verification pipeline
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Verify the X-Actor-Token header and resolve a guardian runtime context.
|
|
66
|
+
*
|
|
67
|
+
* Steps:
|
|
68
|
+
* 1. Extract the header value.
|
|
69
|
+
* 2. Verify HMAC signature and expiration.
|
|
70
|
+
* 3. Check the token hash is active in the actor-token store.
|
|
71
|
+
* 4. Resolve a guardian context through the standard trust pipeline using
|
|
72
|
+
* the claims' guardianPrincipalId as the sender identity.
|
|
73
|
+
*
|
|
74
|
+
* Returns an ok result with claims and guardianContext, or an error with
|
|
75
|
+
* the HTTP status code and message to return.
|
|
76
|
+
*/
|
|
77
|
+
export function verifyHttpActorToken(req: Request): ActorTokenVerification {
|
|
78
|
+
const rawToken = extractActorToken(req);
|
|
79
|
+
if (!rawToken) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
status: 401,
|
|
83
|
+
message: 'Missing X-Actor-Token header. Vellum HTTP requests require actor identity.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Structural + signature verification
|
|
88
|
+
const verifyResult = verifyActorToken(rawToken);
|
|
89
|
+
if (!verifyResult.ok) {
|
|
90
|
+
log.warn({ reason: verifyResult.reason }, 'Actor token verification failed');
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
status: 401,
|
|
94
|
+
message: `Invalid actor token: ${verifyResult.reason}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check the token is active in the store (not revoked)
|
|
99
|
+
const tokenHash = hashToken(rawToken);
|
|
100
|
+
const record = findActiveByTokenHash(tokenHash);
|
|
101
|
+
if (!record) {
|
|
102
|
+
log.warn('Actor token not found in active store (possibly revoked)');
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 401,
|
|
106
|
+
message: 'Actor token is no longer active',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { claims } = verifyResult;
|
|
111
|
+
|
|
112
|
+
// Resolve guardian context through the shared trust pipeline.
|
|
113
|
+
// The guardianPrincipalId from the token is used as the sender identity,
|
|
114
|
+
// and 'vellum' is used as the channel for binding lookup.
|
|
115
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
116
|
+
const guardianCtx = resolveGuardianContext({
|
|
117
|
+
assistantId,
|
|
118
|
+
sourceChannel: 'vellum',
|
|
119
|
+
externalChatId: 'local',
|
|
120
|
+
senderExternalUserId: claims.guardianPrincipalId,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const guardianContext = toGuardianRuntimeContext('vellum' as ChannelId, guardianCtx);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
claims,
|
|
128
|
+
guardianContext,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Verify that the actor identity from a verified token matches the bound
|
|
134
|
+
* guardian for the vellum channel. Used for guardian-decision endpoints
|
|
135
|
+
* where only the guardian should be able to approve/reject.
|
|
136
|
+
*
|
|
137
|
+
* Returns true if the actor is the bound guardian, false otherwise.
|
|
138
|
+
*/
|
|
139
|
+
export function isActorBoundGuardian(claims: ActorTokenClaims): boolean {
|
|
140
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
141
|
+
const binding = getActiveBinding(assistantId, 'vellum');
|
|
142
|
+
if (!binding) return false;
|
|
143
|
+
return binding.guardianExternalUserId === claims.guardianPrincipalId;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Bearer-auth fallback variants
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/** Loopback addresses — used to gate the local identity fallback. */
|
|
151
|
+
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
152
|
+
|
|
153
|
+
/** Bun server shape needed for requestIP — avoids importing the full Bun type. */
|
|
154
|
+
export type ServerWithRequestIP = {
|
|
155
|
+
requestIP(req: Request): { address: string; family: string; port: number } | null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Result for the fallback verification path where the actor token is absent
|
|
160
|
+
* but the request is bearer-authenticated (local trusted client like CLI).
|
|
161
|
+
*/
|
|
162
|
+
export interface ActorTokenLocalFallbackResult {
|
|
163
|
+
ok: true;
|
|
164
|
+
claims: null;
|
|
165
|
+
guardianContext: GuardianRuntimeContext;
|
|
166
|
+
localFallback: true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ActorTokenVerificationWithFallback =
|
|
170
|
+
| ActorTokenResult
|
|
171
|
+
| ActorTokenLocalFallbackResult
|
|
172
|
+
| ActorTokenError;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Verify the actor token with fallback to local IPC identity resolution.
|
|
176
|
+
*
|
|
177
|
+
* When an actor token is present, the full verification pipeline runs.
|
|
178
|
+
* When absent AND the request originates from a loopback address, the
|
|
179
|
+
* request is treated as a trusted local client (e.g. CLI) and we fall
|
|
180
|
+
* back to `resolveLocalIpcGuardianContext()` which produces the same
|
|
181
|
+
* guardian context as the IPC pathway.
|
|
182
|
+
*
|
|
183
|
+
* Two conditions must BOTH be met for the local fallback:
|
|
184
|
+
* 1. No X-Forwarded-For header (rules out gateway-proxied requests).
|
|
185
|
+
* 2. The peer remote address is a loopback address (rules out LAN/container
|
|
186
|
+
* connections when the runtime binds to 0.0.0.0).
|
|
187
|
+
*
|
|
188
|
+
* The peer address is checked via `server.requestIP(req)`.
|
|
189
|
+
*
|
|
190
|
+
* --- CLI compatibility note ---
|
|
191
|
+
*
|
|
192
|
+
* The local fallback is an intentional CLI compatibility path, not a
|
|
193
|
+
* security gap. The CLI currently sends only `Authorization: Bearer <token>`
|
|
194
|
+
* without `X-Actor-Token`. This fallback allows the CLI to function until
|
|
195
|
+
* it is migrated to actor tokens in a future milestone.
|
|
196
|
+
*
|
|
197
|
+
* The fallback is gated by three conditions that together ensure only
|
|
198
|
+
* genuinely local connections receive guardian identity:
|
|
199
|
+
* (1) Absence of X-Forwarded-For header — the gateway always injects
|
|
200
|
+
* this header when proxying, so its presence indicates a remote client.
|
|
201
|
+
* (2) Loopback origin check — verifies the peer IP is 127.0.0.1/::1,
|
|
202
|
+
* preventing LAN or container peers.
|
|
203
|
+
* (3) Valid bearer authentication — already enforced upstream by the
|
|
204
|
+
* HTTP server's auth gate before this function is called.
|
|
205
|
+
*
|
|
206
|
+
* Once the CLI adopts actor tokens, this fallback can be removed.
|
|
207
|
+
*/
|
|
208
|
+
export function verifyHttpActorTokenWithLocalFallback(
|
|
209
|
+
req: Request,
|
|
210
|
+
server: ServerWithRequestIP,
|
|
211
|
+
): ActorTokenVerificationWithFallback {
|
|
212
|
+
const rawToken = extractActorToken(req);
|
|
213
|
+
|
|
214
|
+
// If an actor token is present, use the strict verification pipeline.
|
|
215
|
+
if (rawToken) {
|
|
216
|
+
return verifyHttpActorToken(req);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Gate the local fallback on provably-local origin. The gateway runtime
|
|
220
|
+
// proxy always injects X-Forwarded-For with the real client IP when
|
|
221
|
+
// forwarding requests. Direct local connections (CLI, macOS app) never
|
|
222
|
+
// set this header. If X-Forwarded-For is present, the request was
|
|
223
|
+
// proxied through the gateway on behalf of a potentially remote client
|
|
224
|
+
// and must not receive local guardian identity.
|
|
225
|
+
if (req.headers.get('x-forwarded-for')) {
|
|
226
|
+
log.warn('Rejecting local identity fallback: request has X-Forwarded-For (proxied through gateway)');
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
status: 401,
|
|
230
|
+
message: 'Missing X-Actor-Token header. Proxied requests require actor identity.',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Verify the peer address is actually loopback. This prevents LAN or
|
|
235
|
+
// container peers from receiving local guardian identity when the
|
|
236
|
+
// runtime binds to 0.0.0.0.
|
|
237
|
+
const peerIp = server.requestIP(req)?.address;
|
|
238
|
+
if (!peerIp || !LOOPBACK_ADDRESSES.has(peerIp)) {
|
|
239
|
+
log.warn({ peerIp }, 'Rejecting local identity fallback: peer is not loopback');
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
status: 401,
|
|
243
|
+
message: 'Missing X-Actor-Token header. Non-loopback requests require actor identity.',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// No actor token, no forwarding header, and the peer is on loopback
|
|
248
|
+
// — this is a direct local connection that passed bearer auth at the
|
|
249
|
+
// HTTP server layer. Resolve identity the same way as IPC.
|
|
250
|
+
log.debug('No actor token present on direct local request; using local IPC identity fallback');
|
|
251
|
+
const guardianContext = resolveLocalIpcGuardianContext('vellum');
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
claims: null,
|
|
255
|
+
guardianContext,
|
|
256
|
+
localFallback: true,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check whether the local fallback identity is the bound guardian.
|
|
262
|
+
*
|
|
263
|
+
* When no actor token is present (local fallback), the local user is
|
|
264
|
+
* treated as the guardian of their own machine — equivalent to IPC.
|
|
265
|
+
* This returns true when either the resolved trust class is 'guardian'
|
|
266
|
+
* or no vellum binding exists yet (pre-bootstrap).
|
|
267
|
+
*/
|
|
268
|
+
export function isLocalFallbackBoundGuardian(): boolean {
|
|
269
|
+
const guardianContext = resolveLocalIpcGuardianContext('vellum');
|
|
270
|
+
return guardianContext.trustClass === 'guardian';
|
|
271
|
+
}
|