@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.
Files changed (183) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +40 -3
  3. package/README.md +43 -35
  4. package/package.json +1 -1
  5. package/scripts/ipc/generate-swift.ts +1 -0
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  7. package/src/__tests__/actor-token-service.test.ts +1099 -0
  8. package/src/__tests__/agent-loop.test.ts +51 -0
  9. package/src/__tests__/approval-routes-http.test.ts +2 -0
  10. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  11. package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
  12. package/src/__tests__/call-controller.test.ts +49 -0
  13. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  14. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  15. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  16. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  17. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  18. package/src/__tests__/channel-guardian.test.ts +0 -87
  19. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  20. package/src/__tests__/checker.test.ts +33 -12
  21. package/src/__tests__/config-schema.test.ts +4 -0
  22. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  23. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  24. package/src/__tests__/conversation-routes.test.ts +12 -3
  25. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  26. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  27. package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
  28. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  29. package/src/__tests__/guardian-outbound-http.test.ts +4 -4
  30. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  31. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  32. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  33. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  34. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  35. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  36. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  37. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  38. package/src/__tests__/non-member-access-request.test.ts +131 -8
  39. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  40. package/src/__tests__/notification-decision-strategy.test.ts +62 -2
  41. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  42. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  43. package/src/__tests__/relay-server.test.ts +841 -39
  44. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  45. package/src/__tests__/session-agent-loop.test.ts +1 -0
  46. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  47. package/src/__tests__/session-init.benchmark.test.ts +0 -1
  48. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
  49. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  50. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  51. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  52. package/src/__tests__/tool-executor.test.ts +21 -2
  53. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  54. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  55. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  56. package/src/__tests__/twilio-config.test.ts +2 -13
  57. package/src/agent/loop.ts +1 -1
  58. package/src/approvals/guardian-decision-primitive.ts +10 -2
  59. package/src/approvals/guardian-request-resolvers.ts +128 -9
  60. package/src/calls/call-constants.ts +21 -0
  61. package/src/calls/call-controller.ts +9 -2
  62. package/src/calls/call-domain.ts +28 -7
  63. package/src/calls/call-pointer-message-composer.ts +154 -0
  64. package/src/calls/call-pointer-messages.ts +106 -27
  65. package/src/calls/guardian-dispatch.ts +4 -2
  66. package/src/calls/relay-server.ts +424 -12
  67. package/src/calls/twilio-config.ts +4 -11
  68. package/src/calls/twilio-routes.ts +1 -1
  69. package/src/calls/types.ts +3 -1
  70. package/src/cli.ts +5 -4
  71. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  72. package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
  73. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  74. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  75. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  76. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  77. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  78. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  79. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  80. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  81. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  82. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  83. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
  84. package/src/config/calls-schema.ts +24 -0
  85. package/src/config/env.ts +22 -0
  86. package/src/config/feature-flag-registry.json +8 -0
  87. package/src/config/schema.ts +2 -2
  88. package/src/config/skills.ts +11 -0
  89. package/src/config/system-prompt.ts +11 -1
  90. package/src/config/templates/SOUL.md +2 -0
  91. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  92. package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
  93. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  94. package/src/daemon/call-pointer-generators.ts +59 -0
  95. package/src/daemon/computer-use-session.ts +2 -5
  96. package/src/daemon/handlers/apps.ts +76 -20
  97. package/src/daemon/handlers/config-channels.ts +5 -55
  98. package/src/daemon/handlers/config-inbox.ts +9 -3
  99. package/src/daemon/handlers/config-ingress.ts +28 -3
  100. package/src/daemon/handlers/config-telegram.ts +12 -0
  101. package/src/daemon/handlers/config.ts +2 -6
  102. package/src/daemon/handlers/pairing.ts +2 -0
  103. package/src/daemon/handlers/sessions.ts +48 -3
  104. package/src/daemon/handlers/shared.ts +17 -2
  105. package/src/daemon/ipc-contract/integrations.ts +1 -99
  106. package/src/daemon/ipc-contract/messages.ts +47 -1
  107. package/src/daemon/ipc-contract/notifications.ts +11 -0
  108. package/src/daemon/ipc-contract-inventory.json +2 -4
  109. package/src/daemon/lifecycle.ts +17 -0
  110. package/src/daemon/server.ts +14 -1
  111. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  112. package/src/daemon/session-agent-loop.ts +22 -11
  113. package/src/daemon/session-lifecycle.ts +1 -1
  114. package/src/daemon/session-process.ts +11 -1
  115. package/src/daemon/session-runtime-assembly.ts +3 -0
  116. package/src/daemon/session-surfaces.ts +3 -2
  117. package/src/daemon/session.ts +88 -1
  118. package/src/daemon/tool-side-effects.ts +22 -0
  119. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  120. package/src/home-base/prebuilt/index.html +40 -0
  121. package/src/inbound/platform-callback-registration.ts +157 -0
  122. package/src/memory/canonical-guardian-store.ts +1 -1
  123. package/src/memory/db-init.ts +4 -0
  124. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  125. package/src/memory/migrations/index.ts +1 -0
  126. package/src/memory/schema.ts +16 -0
  127. package/src/messaging/provider-types.ts +24 -0
  128. package/src/messaging/provider.ts +7 -0
  129. package/src/messaging/providers/gmail/adapter.ts +127 -0
  130. package/src/messaging/providers/sms/adapter.ts +40 -37
  131. package/src/notifications/adapters/macos.ts +45 -2
  132. package/src/notifications/broadcaster.ts +16 -0
  133. package/src/notifications/copy-composer.ts +39 -1
  134. package/src/notifications/decision-engine.ts +22 -9
  135. package/src/notifications/destination-resolver.ts +16 -2
  136. package/src/notifications/emit-signal.ts +16 -8
  137. package/src/notifications/guardian-question-mode.ts +419 -0
  138. package/src/notifications/signal.ts +14 -3
  139. package/src/permissions/checker.ts +13 -1
  140. package/src/permissions/prompter.ts +14 -0
  141. package/src/providers/anthropic/client.ts +20 -0
  142. package/src/providers/provider-send-message.ts +15 -3
  143. package/src/runtime/access-request-helper.ts +71 -1
  144. package/src/runtime/actor-token-service.ts +234 -0
  145. package/src/runtime/actor-token-store.ts +236 -0
  146. package/src/runtime/channel-approvals.ts +5 -3
  147. package/src/runtime/channel-readiness-service.ts +23 -64
  148. package/src/runtime/channel-readiness-types.ts +3 -4
  149. package/src/runtime/channel-retry-sweep.ts +4 -1
  150. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  151. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  152. package/src/runtime/guardian-context-resolver.ts +82 -0
  153. package/src/runtime/guardian-outbound-actions.ts +0 -3
  154. package/src/runtime/guardian-reply-router.ts +67 -30
  155. package/src/runtime/guardian-vellum-migration.ts +57 -0
  156. package/src/runtime/http-server.ts +65 -12
  157. package/src/runtime/http-types.ts +13 -0
  158. package/src/runtime/invite-redemption-service.ts +8 -0
  159. package/src/runtime/local-actor-identity.ts +76 -0
  160. package/src/runtime/middleware/actor-token.ts +271 -0
  161. package/src/runtime/routes/approval-routes.ts +82 -7
  162. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  163. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  164. package/src/runtime/routes/conversation-routes.ts +140 -52
  165. package/src/runtime/routes/events-routes.ts +20 -5
  166. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  167. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  168. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  169. package/src/runtime/routes/inbound-message-handler.ts +143 -2
  170. package/src/runtime/routes/integration-routes.ts +7 -15
  171. package/src/runtime/routes/pairing-routes.ts +163 -0
  172. package/src/runtime/routes/twilio-routes.ts +934 -0
  173. package/src/runtime/tool-grant-request-helper.ts +3 -1
  174. package/src/security/oauth2.ts +27 -2
  175. package/src/security/token-manager.ts +46 -10
  176. package/src/tools/browser/browser-execution.ts +4 -3
  177. package/src/tools/browser/browser-handoff.ts +10 -18
  178. package/src/tools/browser/browser-manager.ts +80 -25
  179. package/src/tools/browser/browser-screencast.ts +35 -119
  180. package/src/tools/permission-checker.ts +15 -4
  181. package/src/tools/tool-approval-handler.ts +242 -18
  182. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  183. 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 === 'events' && req.method === 'GET') return handleSubscribeAssistantEvents(req, url);
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
+ }