@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.
Files changed (221) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +124 -10
  3. package/README.md +43 -35
  4. package/docs/trusted-contact-access.md +20 -0
  5. package/package.json +1 -1
  6. package/scripts/ipc/generate-swift.ts +1 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  8. package/src/__tests__/access-request-decision.test.ts +0 -1
  9. package/src/__tests__/actor-token-service.test.ts +1099 -0
  10. package/src/__tests__/agent-loop.test.ts +51 -0
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
  14. package/src/__tests__/call-controller.test.ts +49 -0
  15. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  16. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  17. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  18. package/src/__tests__/call-routes-http.test.ts +0 -25
  19. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  21. package/src/__tests__/channel-guardian.test.ts +0 -86
  22. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  23. package/src/__tests__/checker.test.ts +33 -12
  24. package/src/__tests__/config-schema.test.ts +6 -0
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  27. package/src/__tests__/conversation-routes.test.ts +12 -3
  28. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  29. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  30. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  31. package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
  32. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  33. package/src/__tests__/guardian-outbound-http.test.ts +4 -5
  34. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  35. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  36. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  37. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  38. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  39. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  40. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  42. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  43. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  44. package/src/__tests__/non-member-access-request.test.ts +159 -9
  45. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  46. package/src/__tests__/notification-decision-strategy.test.ts +106 -2
  47. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  48. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  49. package/src/__tests__/relay-server.test.ts +1475 -33
  50. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  51. package/src/__tests__/session-agent-loop.test.ts +1 -0
  52. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  53. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  54. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  55. package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
  56. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  57. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  58. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  59. package/src/__tests__/tool-executor.test.ts +21 -2
  60. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  61. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  62. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  63. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  64. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  65. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  66. package/src/__tests__/twilio-config.test.ts +2 -13
  67. package/src/__tests__/twilio-routes.test.ts +4 -3
  68. package/src/__tests__/update-bulletin.test.ts +0 -1
  69. package/src/agent/loop.ts +1 -1
  70. package/src/approvals/guardian-decision-primitive.ts +12 -3
  71. package/src/approvals/guardian-request-resolvers.ts +169 -11
  72. package/src/calls/call-constants.ts +29 -0
  73. package/src/calls/call-controller.ts +11 -3
  74. package/src/calls/call-domain.ts +33 -11
  75. package/src/calls/call-pointer-message-composer.ts +154 -0
  76. package/src/calls/call-pointer-messages.ts +106 -27
  77. package/src/calls/guardian-dispatch.ts +4 -2
  78. package/src/calls/relay-server.ts +921 -112
  79. package/src/calls/twilio-config.ts +4 -11
  80. package/src/calls/twilio-routes.ts +4 -6
  81. package/src/calls/types.ts +3 -1
  82. package/src/calls/voice-session-bridge.ts +4 -3
  83. package/src/cli/core-commands.ts +7 -4
  84. package/src/cli.ts +5 -4
  85. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  86. package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
  87. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  88. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  89. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  90. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  91. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  92. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  93. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  94. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  96. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  97. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
  98. package/src/config/calls-schema.ts +36 -0
  99. package/src/config/env.ts +22 -0
  100. package/src/config/feature-flag-registry.json +8 -8
  101. package/src/config/schema.ts +2 -2
  102. package/src/config/skills.ts +11 -0
  103. package/src/config/system-prompt.ts +11 -1
  104. package/src/config/templates/SOUL.md +2 -0
  105. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  106. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
  107. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  108. package/src/daemon/call-pointer-generators.ts +59 -0
  109. package/src/daemon/computer-use-session.ts +2 -5
  110. package/src/daemon/handlers/apps.ts +76 -20
  111. package/src/daemon/handlers/config-channels.ts +9 -61
  112. package/src/daemon/handlers/config-inbox.ts +11 -3
  113. package/src/daemon/handlers/config-ingress.ts +28 -3
  114. package/src/daemon/handlers/config-telegram.ts +12 -0
  115. package/src/daemon/handlers/config.ts +2 -6
  116. package/src/daemon/handlers/index.ts +2 -1
  117. package/src/daemon/handlers/pairing.ts +2 -0
  118. package/src/daemon/handlers/publish.ts +11 -46
  119. package/src/daemon/handlers/sessions.ts +59 -5
  120. package/src/daemon/handlers/shared.ts +17 -2
  121. package/src/daemon/ipc-contract/apps.ts +1 -0
  122. package/src/daemon/ipc-contract/inbox.ts +4 -0
  123. package/src/daemon/ipc-contract/integrations.ts +1 -97
  124. package/src/daemon/ipc-contract/messages.ts +47 -1
  125. package/src/daemon/ipc-contract/notifications.ts +11 -0
  126. package/src/daemon/ipc-contract-inventory.json +2 -4
  127. package/src/daemon/lifecycle.ts +17 -0
  128. package/src/daemon/server.ts +16 -2
  129. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  130. package/src/daemon/session-agent-loop.ts +24 -12
  131. package/src/daemon/session-lifecycle.ts +1 -1
  132. package/src/daemon/session-process.ts +11 -1
  133. package/src/daemon/session-runtime-assembly.ts +6 -1
  134. package/src/daemon/session-surfaces.ts +32 -3
  135. package/src/daemon/session.ts +88 -1
  136. package/src/daemon/tool-side-effects.ts +22 -0
  137. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  138. package/src/home-base/prebuilt/index.html +40 -0
  139. package/src/inbound/platform-callback-registration.ts +157 -0
  140. package/src/memory/canonical-guardian-store.ts +1 -1
  141. package/src/memory/conversation-crud.ts +2 -1
  142. package/src/memory/conversation-title-service.ts +16 -2
  143. package/src/memory/db-init.ts +8 -0
  144. package/src/memory/delivery-crud.ts +2 -1
  145. package/src/memory/guardian-action-store.ts +2 -1
  146. package/src/memory/guardian-approvals.ts +3 -2
  147. package/src/memory/ingress-invite-store.ts +12 -2
  148. package/src/memory/ingress-member-store.ts +4 -3
  149. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  150. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  151. package/src/memory/migrations/index.ts +2 -0
  152. package/src/memory/schema.ts +26 -5
  153. package/src/messaging/provider-types.ts +24 -0
  154. package/src/messaging/provider.ts +7 -0
  155. package/src/messaging/providers/gmail/adapter.ts +127 -0
  156. package/src/messaging/providers/sms/adapter.ts +40 -37
  157. package/src/notifications/adapters/macos.ts +45 -2
  158. package/src/notifications/broadcaster.ts +16 -0
  159. package/src/notifications/copy-composer.ts +50 -2
  160. package/src/notifications/decision-engine.ts +22 -9
  161. package/src/notifications/destination-resolver.ts +16 -2
  162. package/src/notifications/emit-signal.ts +18 -9
  163. package/src/notifications/guardian-question-mode.ts +419 -0
  164. package/src/notifications/signal.ts +14 -3
  165. package/src/permissions/checker.ts +13 -1
  166. package/src/permissions/prompter.ts +14 -0
  167. package/src/providers/anthropic/client.ts +20 -0
  168. package/src/providers/provider-send-message.ts +15 -3
  169. package/src/runtime/access-request-helper.ts +82 -4
  170. package/src/runtime/actor-token-service.ts +234 -0
  171. package/src/runtime/actor-token-store.ts +236 -0
  172. package/src/runtime/actor-trust-resolver.ts +2 -2
  173. package/src/runtime/assistant-scope.ts +10 -0
  174. package/src/runtime/channel-approvals.ts +5 -3
  175. package/src/runtime/channel-readiness-service.ts +23 -64
  176. package/src/runtime/channel-readiness-types.ts +3 -4
  177. package/src/runtime/channel-retry-sweep.ts +4 -1
  178. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  179. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  180. package/src/runtime/guardian-context-resolver.ts +82 -0
  181. package/src/runtime/guardian-outbound-actions.ts +5 -7
  182. package/src/runtime/guardian-reply-router.ts +67 -30
  183. package/src/runtime/guardian-vellum-migration.ts +57 -0
  184. package/src/runtime/http-server.ts +75 -31
  185. package/src/runtime/http-types.ts +13 -0
  186. package/src/runtime/ingress-service.ts +14 -0
  187. package/src/runtime/invite-redemption-service.ts +10 -1
  188. package/src/runtime/local-actor-identity.ts +76 -0
  189. package/src/runtime/middleware/actor-token.ts +271 -0
  190. package/src/runtime/middleware/twilio-validation.ts +2 -4
  191. package/src/runtime/routes/approval-routes.ts +82 -7
  192. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  193. package/src/runtime/routes/call-routes.ts +2 -1
  194. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  195. package/src/runtime/routes/channel-route-shared.ts +3 -3
  196. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  197. package/src/runtime/routes/conversation-routes.ts +142 -53
  198. package/src/runtime/routes/events-routes.ts +22 -8
  199. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  200. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  201. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  202. package/src/runtime/routes/inbound-conversation.ts +4 -3
  203. package/src/runtime/routes/inbound-message-handler.ts +147 -5
  204. package/src/runtime/routes/ingress-routes.ts +2 -0
  205. package/src/runtime/routes/integration-routes.ts +7 -15
  206. package/src/runtime/routes/pairing-routes.ts +163 -0
  207. package/src/runtime/routes/twilio-routes.ts +934 -0
  208. package/src/runtime/tool-grant-request-helper.ts +3 -1
  209. package/src/security/oauth2.ts +27 -2
  210. package/src/security/token-manager.ts +46 -10
  211. package/src/tools/browser/browser-execution.ts +4 -3
  212. package/src/tools/browser/browser-handoff.ts +10 -18
  213. package/src/tools/browser/browser-manager.ts +80 -25
  214. package/src/tools/browser/browser-screencast.ts +35 -119
  215. package/src/tools/calls/call-start.ts +2 -1
  216. package/src/tools/permission-checker.ts +15 -4
  217. package/src/tools/terminal/parser.ts +12 -0
  218. package/src/tools/tool-approval-handler.ts +244 -19
  219. package/src/workspace/git-service.ts +19 -0
  220. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  221. package/src/daemon/handlers/config-twilio.ts +0 -1082
@@ -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
+ }
@@ -12,12 +12,10 @@ import { httpError } from '../http-errors.js';
12
12
  const log = getLogger('runtime-http');
13
13
 
14
14
  /**
15
- * Regex to extract the Twilio webhook subpath from both top-level and
16
- * assistant-scoped route shapes:
15
+ * Regex to extract the Twilio webhook subpath:
17
16
  * /v1/calls/twilio/<subpath>
18
- * /v1/assistants/<id>/calls/twilio/<subpath>
19
17
  */
20
- export const TWILIO_WEBHOOK_RE = /^\/v1\/(?:assistants\/[^/]+\/)?calls\/twilio\/(.+)$/;
18
+ export const TWILIO_WEBHOOK_RE = /^\/v1\/calls\/twilio\/(.+)$/;
21
19
 
22
20
  /**
23
21
  * Gateway-compatible Twilio webhook paths:
@@ -3,20 +3,77 @@
3
3
  *
4
4
  * These endpoints resolve pending confirmations, secrets, and trust rules
5
5
  * by requestId — orthogonal to message sending.
6
+ *
7
+ * All approval endpoints require a valid actor token via the X-Actor-Token
8
+ * header (with local CLI fallback). Guardian decisions additionally verify
9
+ * that the actor is the bound guardian.
6
10
  */
7
11
  import { getConversationByKey } from '../../memory/conversation-key-store.js';
8
12
  import { addRule } from '../../permissions/trust-store.js';
9
13
  import { getTool } from '../../tools/registry.js';
10
14
  import { getLogger } from '../../util/logger.js';
11
15
  import { httpError } from '../http-errors.js';
16
+ import {
17
+ isActorBoundGuardian,
18
+ isLocalFallbackBoundGuardian,
19
+ type ServerWithRequestIP,
20
+ verifyHttpActorTokenWithLocalFallback,
21
+ } from '../middleware/actor-token.js';
12
22
  import * as pendingInteractions from '../pending-interactions.js';
13
23
 
14
24
  const log = getLogger('approval-routes');
15
25
 
26
+ /**
27
+ * Verify the actor token from the request with local fallback.
28
+ * Returns an error Response if verification fails, or null if
29
+ * the actor is authenticated (via actor token or local identity).
30
+ */
31
+ function requireActorToken(req: Request, server: ServerWithRequestIP): Response | null {
32
+ const result = verifyHttpActorTokenWithLocalFallback(req, server);
33
+ if (!result.ok) {
34
+ return httpError(
35
+ result.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
36
+ result.message,
37
+ result.status,
38
+ );
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Verify the actor token and confirm the actor is the bound guardian.
45
+ * When no actor token is present (bearer-authenticated local client),
46
+ * falls back to local IPC identity resolution and checks the local
47
+ * identity is the bound guardian.
48
+ */
49
+ function requireBoundGuardian(req: Request, server: ServerWithRequestIP): Response | null {
50
+ const result = verifyHttpActorTokenWithLocalFallback(req, server);
51
+ if (!result.ok) {
52
+ return httpError(
53
+ result.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
54
+ result.message,
55
+ result.status,
56
+ );
57
+ }
58
+ // For actor-token-authenticated requests, check the token's identity.
59
+ // For local fallback (bearer-auth only), check the local identity.
60
+ const isBoundGuardian = result.claims
61
+ ? isActorBoundGuardian(result.claims)
62
+ : isLocalFallbackBoundGuardian();
63
+ if (!isBoundGuardian) {
64
+ return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
65
+ }
66
+ return null;
67
+ }
68
+
16
69
  /**
17
70
  * POST /v1/confirm — resolve a pending confirmation by requestId.
71
+ * Requires a valid actor token (guardian-bound).
18
72
  */
19
- export async function handleConfirm(req: Request): Promise<Response> {
73
+ export async function handleConfirm(req: Request, server: ServerWithRequestIP): Promise<Response> {
74
+ const authError = requireBoundGuardian(req, server);
75
+ if (authError) return authError;
76
+
20
77
  const body = await req.json() as {
21
78
  requestId?: string;
22
79
  decision?: string;
@@ -37,14 +94,20 @@ export async function handleConfirm(req: Request): Promise<Response> {
37
94
  return httpError('NOT_FOUND', 'No pending interaction found for this requestId', 404);
38
95
  }
39
96
 
40
- interaction.session.handleConfirmationResponse(requestId, decision);
97
+ interaction.session.handleConfirmationResponse(requestId, decision, undefined, undefined, undefined, {
98
+ source: 'button',
99
+ });
41
100
  return Response.json({ accepted: true });
42
101
  }
43
102
 
44
103
  /**
45
104
  * POST /v1/secret — resolve a pending secret request by requestId.
105
+ * Requires a valid actor token (guardian-bound).
46
106
  */
47
- export async function handleSecret(req: Request): Promise<Response> {
107
+ export async function handleSecret(req: Request, server: ServerWithRequestIP): Promise<Response> {
108
+ const authError = requireBoundGuardian(req, server);
109
+ if (authError) return authError;
110
+
48
111
  const body = await req.json() as {
49
112
  requestId?: string;
50
113
  value?: string;
@@ -76,13 +139,17 @@ export async function handleSecret(req: Request): Promise<Response> {
76
139
 
77
140
  /**
78
141
  * POST /v1/trust-rules — add a trust rule for a pending confirmation.
142
+ * Requires a valid actor token (guardian-bound).
79
143
  *
80
144
  * Does NOT resolve the confirmation itself (the client still needs to
81
145
  * POST /v1/confirm to approve/deny). Validates the pattern and scope
82
146
  * against the server-provided allowlist options from the original
83
147
  * confirmation_request.
84
148
  */
85
- export async function handleTrustRule(req: Request): Promise<Response> {
149
+ export async function handleTrustRule(req: Request, server: ServerWithRequestIP): Promise<Response> {
150
+ const authError = requireBoundGuardian(req, server);
151
+ if (authError) return authError;
152
+
86
153
  const body = await req.json() as {
87
154
  requestId?: string;
88
155
  pattern?: string;
@@ -130,9 +197,14 @@ export async function handleTrustRule(req: Request): Promise<Response> {
130
197
  return httpError('FORBIDDEN', 'pattern does not match any server-provided allowlist option', 403);
131
198
  }
132
199
 
133
- // Validate scope against server-provided scope options
200
+ // Validate scope against server-provided scope options.
201
+ // Non-scoped tools have empty scopeOptions — only "everywhere" is valid for them.
134
202
  const validScopes = (confirmation.scopeOptions ?? []).map((o) => o.scope);
135
- if (!validScopes.includes(scope)) {
203
+ if (validScopes.length === 0) {
204
+ if (scope !== 'everywhere') {
205
+ return httpError('FORBIDDEN', 'non-scoped tools only accept scope "everywhere"', 403);
206
+ }
207
+ } else if (!validScopes.includes(scope)) {
136
208
  return httpError('FORBIDDEN', 'scope does not match any server-provided scope option', 403);
137
209
  }
138
210
 
@@ -155,12 +227,15 @@ export async function handleTrustRule(req: Request): Promise<Response> {
155
227
 
156
228
  /**
157
229
  * GET /v1/pending-interactions?conversationKey=...
230
+ * Requires a valid actor token.
158
231
  *
159
232
  * Returns pending confirmations and secrets for a conversation, allowing
160
233
  * polling-based clients (like the CLI) to discover approval requests
161
234
  * without SSE.
162
235
  */
163
- export function handleListPendingInteractions(url: URL): Response {
236
+ export function handleListPendingInteractions(url: URL, req: Request, server: ServerWithRequestIP): Response {
237
+ const authError = requireActorToken(req, server);
238
+ if (authError) return authError;
164
239
  const conversationKey = url.searchParams.get('conversationKey');
165
240
  const conversationId = url.searchParams.get('conversationId');
166
241