@vellumai/assistant 0.4.13 → 0.4.15

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 (133) hide show
  1. package/ARCHITECTURE.md +77 -38
  2. package/README.md +10 -12
  3. package/package.json +1 -1
  4. package/src/__tests__/actor-token-service.test.ts +108 -522
  5. package/src/__tests__/channel-approval-routes.test.ts +92 -239
  6. package/src/__tests__/channel-approval.test.ts +100 -0
  7. package/src/__tests__/conversation-routes-guardian-reply.test.ts +13 -6
  8. package/src/__tests__/conversation-routes.test.ts +11 -4
  9. package/src/__tests__/guardian-actions-endpoint.test.ts +26 -19
  10. package/src/__tests__/mcp-health-check.test.ts +65 -0
  11. package/src/__tests__/permission-types.test.ts +33 -0
  12. package/src/__tests__/scan-result-store.test.ts +121 -0
  13. package/src/__tests__/session-agent-loop.test.ts +120 -0
  14. package/src/__tests__/session-approval-overrides.test.ts +205 -0
  15. package/src/__tests__/session-surfaces-task-progress.test.ts +38 -0
  16. package/src/amazon/client.ts +8 -5
  17. package/src/approvals/guardian-decision-primitive.ts +14 -9
  18. package/src/approvals/guardian-request-resolvers.ts +2 -2
  19. package/src/calls/call-controller.ts +2 -2
  20. package/src/calls/twilio-routes.ts +2 -2
  21. package/src/cli/mcp.ts +3 -3
  22. package/src/cli.ts +24 -0
  23. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +19 -130
  24. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +8 -6
  25. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -1
  26. package/src/config/bundled-skills/messaging/SKILL.md +49 -14
  27. package/src/config/bundled-skills/messaging/TOOLS.json +52 -9
  28. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +35 -11
  29. package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +3 -1
  30. package/src/config/bundled-skills/messaging/tools/gmail-forward.ts +5 -6
  31. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +10 -2
  32. package/src/config/bundled-skills/messaging/tools/gmail-send-draft.ts +20 -0
  33. package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +3 -4
  34. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -8
  35. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +76 -0
  36. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +10 -0
  37. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +11 -3
  38. package/src/config/bundled-skills/messaging/tools/scan-result-store.ts +86 -0
  39. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  40. package/src/config/bundled-skills/skills-catalog/SKILL.md +31 -8
  41. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  42. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  43. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  44. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  45. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +79 -24
  46. package/src/config/bundled-skills/sms-setup/SKILL.md +1 -1
  47. package/src/config/bundled-skills/telegram-setup/SKILL.md +1 -1
  48. package/src/config/bundled-skills/twilio-setup/SKILL.md +1 -1
  49. package/src/daemon/approval-generators.ts +6 -3
  50. package/src/daemon/handlers/config-ingress.ts +2 -6
  51. package/src/daemon/handlers/guardian-actions.ts +1 -1
  52. package/src/daemon/handlers/sessions.ts +4 -1
  53. package/src/daemon/handlers/shared.ts +3 -0
  54. package/src/daemon/handlers/skills.ts +32 -0
  55. package/src/daemon/ipc-contract/messages.ts +3 -1
  56. package/src/daemon/ipc-handler.ts +24 -0
  57. package/src/daemon/ipc-validate.ts +1 -1
  58. package/src/daemon/lifecycle.ts +6 -8
  59. package/src/daemon/server.ts +8 -3
  60. package/src/daemon/session-agent-loop.ts +19 -1
  61. package/src/daemon/session-attachments.ts +2 -1
  62. package/src/daemon/session-history.ts +2 -2
  63. package/src/daemon/session-process.ts +5 -9
  64. package/src/daemon/session-surfaces.ts +17 -1
  65. package/src/daemon/session-tool-setup.ts +216 -69
  66. package/src/daemon/session.ts +24 -1
  67. package/src/events/domain-events.ts +1 -1
  68. package/src/events/tool-domain-event-publisher.ts +5 -10
  69. package/src/influencer/client.ts +8 -7
  70. package/src/messaging/providers/gmail/client.ts +33 -1
  71. package/src/messaging/providers/gmail/mime-builder.ts +5 -1
  72. package/src/messaging/providers/sms/adapter.ts +3 -7
  73. package/src/messaging/providers/telegram-bot/adapter.ts +3 -7
  74. package/src/messaging/providers/whatsapp/adapter.ts +3 -7
  75. package/src/notifications/adapters/sms.ts +2 -2
  76. package/src/notifications/adapters/telegram.ts +2 -2
  77. package/src/permissions/prompter.ts +2 -0
  78. package/src/permissions/types.ts +11 -1
  79. package/src/runtime/approval-conversation-turn.ts +4 -0
  80. package/src/runtime/auth/__tests__/context.test.ts +130 -0
  81. package/src/runtime/auth/__tests__/credential-service.test.ts +277 -0
  82. package/src/runtime/auth/__tests__/guard-tests.test.ts +289 -0
  83. package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +71 -0
  84. package/src/runtime/auth/__tests__/middleware.test.ts +239 -0
  85. package/src/runtime/auth/__tests__/policy.test.ts +29 -0
  86. package/src/runtime/auth/__tests__/route-policy.test.ts +166 -0
  87. package/src/runtime/auth/__tests__/scopes.test.ts +109 -0
  88. package/src/runtime/auth/__tests__/subject.test.ts +149 -0
  89. package/src/runtime/auth/__tests__/token-service.test.ts +263 -0
  90. package/src/runtime/auth/context.ts +62 -0
  91. package/src/runtime/{actor-refresh-token-service.ts → auth/credential-service.ts} +112 -79
  92. package/src/runtime/auth/external-assistant-id.ts +69 -0
  93. package/src/runtime/auth/index.ts +37 -0
  94. package/src/runtime/auth/middleware.ts +127 -0
  95. package/src/runtime/auth/policy.ts +17 -0
  96. package/src/runtime/auth/route-policy.ts +261 -0
  97. package/src/runtime/auth/scopes.ts +64 -0
  98. package/src/runtime/auth/subject.ts +68 -0
  99. package/src/runtime/auth/token-service.ts +275 -0
  100. package/src/runtime/auth/types.ts +79 -0
  101. package/src/runtime/channel-approval-parser.ts +11 -5
  102. package/src/runtime/channel-approval-types.ts +1 -1
  103. package/src/runtime/channel-approvals.ts +22 -1
  104. package/src/runtime/guardian-action-followup-executor.ts +2 -2
  105. package/src/runtime/guardian-context-resolver.ts +15 -0
  106. package/src/runtime/guardian-decision-types.ts +23 -6
  107. package/src/runtime/guardian-outbound-actions.ts +4 -22
  108. package/src/runtime/guardian-reply-router.ts +5 -3
  109. package/src/runtime/http-server.ts +210 -182
  110. package/src/runtime/http-types.ts +11 -1
  111. package/src/runtime/local-actor-identity.ts +25 -0
  112. package/src/runtime/pending-interactions.ts +1 -0
  113. package/src/runtime/routes/approval-routes.ts +42 -59
  114. package/src/runtime/routes/channel-route-shared.ts +9 -41
  115. package/src/runtime/routes/channel-routes.ts +0 -2
  116. package/src/runtime/routes/conversation-routes.ts +39 -49
  117. package/src/runtime/routes/events-routes.ts +15 -22
  118. package/src/runtime/routes/guardian-action-routes.ts +46 -51
  119. package/src/runtime/routes/guardian-approval-interception.ts +6 -5
  120. package/src/runtime/routes/guardian-bootstrap-routes.ts +12 -8
  121. package/src/runtime/routes/guardian-refresh-routes.ts +2 -2
  122. package/src/runtime/routes/inbound-message-handler.ts +39 -45
  123. package/src/runtime/routes/pairing-routes.ts +9 -9
  124. package/src/runtime/routes/secret-routes.ts +90 -45
  125. package/src/runtime/routes/surface-action-routes.ts +12 -2
  126. package/src/runtime/routes/trust-rules-routes.ts +13 -0
  127. package/src/runtime/routes/twilio-routes.ts +3 -3
  128. package/src/runtime/session-approval-overrides.ts +86 -0
  129. package/src/security/keychain-to-encrypted-migration.ts +8 -1
  130. package/src/skills/frontmatter.ts +44 -1
  131. package/src/tools/permission-checker.ts +226 -74
  132. package/src/runtime/actor-token-service.ts +0 -234
  133. package/src/runtime/middleware/actor-token.ts +0 -265
@@ -4,63 +4,43 @@
4
4
  * These endpoints resolve pending confirmations, secrets, and trust rules
5
5
  * by requestId — orthogonal to message sending.
6
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.
7
+ * All approval endpoints require a valid JWT via the Authorization: Bearer
8
+ * header. Guardian decisions additionally verify that the actor is the
9
+ * bound guardian.
10
10
  */
11
+ import { isHttpAuthDisabled } from '../../config/env.js';
11
12
  import { getConversationByKey } from '../../memory/conversation-key-store.js';
13
+ import { getActiveBinding } from '../../memory/guardian-bindings.js';
12
14
  import { addRule } from '../../permissions/trust-store.js';
15
+ import type { UserDecision } from '../../permissions/types.js';
13
16
  import { getTool } from '../../tools/registry.js';
14
17
  import { getLogger } from '../../util/logger.js';
18
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
19
+ import type { AuthContext } from '../auth/types.js';
15
20
  import { httpError } from '../http-errors.js';
16
- import {
17
- isActorBoundGuardian,
18
- isLocalFallbackBoundGuardian,
19
- type ServerWithRequestIP,
20
- verifyHttpActorTokenWithLocalFallback,
21
- } from '../middleware/actor-token.js';
22
21
  import * as pendingInteractions from '../pending-interactions.js';
23
22
 
24
23
  const log = getLogger('approval-routes');
25
24
 
26
25
  /**
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).
26
+ * Verify the actor from AuthContext is the bound guardian for the vellum channel.
27
+ * Returns an error Response if not, or null if allowed.
30
28
  */
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
- );
29
+ function requireBoundGuardian(authContext: AuthContext): Response | null {
30
+ // Dev bypass: when auth is disabled, skip guardian binding check
31
+ // (mirrors enforcePolicy dev bypass in route-policy.ts)
32
+ if (isHttpAuthDisabled()) {
33
+ return null;
39
34
  }
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
- );
35
+ if (!authContext.actorPrincipalId) {
36
+ return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
37
+ }
38
+ const binding = getActiveBinding(DAEMON_INTERNAL_ASSISTANT_ID, 'vellum');
39
+ if (!binding) {
40
+ // No binding yet in pre-bootstrap state, allow through
41
+ return null;
57
42
  }
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) {
43
+ if (binding.guardianExternalUserId !== authContext.actorPrincipalId) {
64
44
  return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
65
45
  }
66
46
  return null;
@@ -68,10 +48,10 @@ function requireBoundGuardian(req: Request, server: ServerWithRequestIP): Respon
68
48
 
69
49
  /**
70
50
  * POST /v1/confirm — resolve a pending confirmation by requestId.
71
- * Requires a valid actor token (guardian-bound).
51
+ * Requires AuthContext with guardian-bound actor.
72
52
  */
73
- export async function handleConfirm(req: Request, server: ServerWithRequestIP): Promise<Response> {
74
- const authError = requireBoundGuardian(req, server);
53
+ export async function handleConfirm(req: Request, authContext: AuthContext): Promise<Response> {
54
+ const authError = requireBoundGuardian(authContext);
75
55
  if (authError) return authError;
76
56
 
77
57
  const body = await req.json() as {
@@ -85,8 +65,9 @@ export async function handleConfirm(req: Request, server: ServerWithRequestIP):
85
65
  return httpError('BAD_REQUEST', 'requestId is required', 400);
86
66
  }
87
67
 
88
- if (decision !== 'allow' && decision !== 'deny') {
89
- return httpError('BAD_REQUEST', 'decision must be "allow" or "deny"', 400);
68
+ const validConfirmDecisions = ['allow', 'allow_10m', 'allow_thread', 'deny'];
69
+ if (typeof decision !== 'string' || !validConfirmDecisions.includes(decision)) {
70
+ return httpError('BAD_REQUEST', `decision must be one of: ${validConfirmDecisions.join(', ')}`, 400);
90
71
  }
91
72
 
92
73
  const interaction = pendingInteractions.resolve(requestId);
@@ -94,7 +75,7 @@ export async function handleConfirm(req: Request, server: ServerWithRequestIP):
94
75
  return httpError('NOT_FOUND', 'No pending interaction found for this requestId', 404);
95
76
  }
96
77
 
97
- interaction.session.handleConfirmationResponse(requestId, decision, undefined, undefined, undefined, {
78
+ interaction.session.handleConfirmationResponse(requestId, decision as UserDecision, undefined, undefined, undefined, {
98
79
  source: 'button',
99
80
  });
100
81
  return Response.json({ accepted: true });
@@ -102,10 +83,10 @@ export async function handleConfirm(req: Request, server: ServerWithRequestIP):
102
83
 
103
84
  /**
104
85
  * POST /v1/secret — resolve a pending secret request by requestId.
105
- * Requires a valid actor token (guardian-bound).
86
+ * Requires AuthContext with guardian-bound actor.
106
87
  */
107
- export async function handleSecret(req: Request, server: ServerWithRequestIP): Promise<Response> {
108
- const authError = requireBoundGuardian(req, server);
88
+ export async function handleSecret(req: Request, authContext: AuthContext): Promise<Response> {
89
+ const authError = requireBoundGuardian(authContext);
109
90
  if (authError) return authError;
110
91
 
111
92
  const body = await req.json() as {
@@ -139,15 +120,15 @@ export async function handleSecret(req: Request, server: ServerWithRequestIP): P
139
120
 
140
121
  /**
141
122
  * POST /v1/trust-rules — add a trust rule for a pending confirmation.
142
- * Requires a valid actor token (guardian-bound).
123
+ * Requires AuthContext with guardian-bound actor.
143
124
  *
144
125
  * Does NOT resolve the confirmation itself (the client still needs to
145
126
  * POST /v1/confirm to approve/deny). Validates the pattern and scope
146
127
  * against the server-provided allowlist options from the original
147
128
  * confirmation_request.
148
129
  */
149
- export async function handleTrustRule(req: Request, server: ServerWithRequestIP): Promise<Response> {
150
- const authError = requireBoundGuardian(req, server);
130
+ export async function handleTrustRule(req: Request, authContext: AuthContext): Promise<Response> {
131
+ const authError = requireBoundGuardian(authContext);
151
132
  if (authError) return authError;
152
133
 
153
134
  const body = await req.json() as {
@@ -227,15 +208,16 @@ export async function handleTrustRule(req: Request, server: ServerWithRequestIP)
227
208
 
228
209
  /**
229
210
  * GET /v1/pending-interactions?conversationKey=...
230
- * Requires a valid actor token.
211
+ * Requires AuthContext (already verified upstream by JWT middleware).
231
212
  *
232
213
  * Returns pending confirmations and secrets for a conversation, allowing
233
214
  * polling-based clients (like the CLI) to discover approval requests
234
215
  * without SSE.
235
216
  */
236
- export function handleListPendingInteractions(url: URL, req: Request, server: ServerWithRequestIP): Response {
237
- const authError = requireActorToken(req, server);
238
- if (authError) return authError;
217
+ export function handleListPendingInteractions(url: URL, _authContext: AuthContext): Response {
218
+ // Auth is already verified by JWT middleware upstream — no additional
219
+ // verification needed here. The _authContext parameter is accepted for
220
+ // type consistency and potential future use.
239
221
  const conversationKey = url.searchParams.get('conversationKey');
240
222
  const conversationId = url.searchParams.get('conversationId');
241
223
 
@@ -273,6 +255,7 @@ export function handleListPendingInteractions(url: URL, req: Request, server: Se
273
255
  })),
274
256
  scopeOptions: confirmation.confirmationDetails?.scopeOptions,
275
257
  persistentDecisionsAllowed: confirmation.confirmationDetails?.persistentDecisionsAllowed,
258
+ temporaryOptionsAvailable: confirmation.confirmationDetails?.temporaryOptionsAvailable,
276
259
  }
277
260
  : null,
278
261
  pendingSecret: secret
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * Shared types, constants, and utilities used across channel route modules.
3
3
  */
4
- import { timingSafeEqual } from 'node:crypto';
5
-
6
4
  import type { ChannelId } from '../../channels/types.js';
7
5
  import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
8
6
  import type {
@@ -18,44 +16,6 @@ export function canonicalChannelAssistantId(_assistantId: string): string {
18
16
  return DAEMON_INTERNAL_ASSISTANT_ID;
19
17
  }
20
18
 
21
- // ---------------------------------------------------------------------------
22
- // Gateway-origin proof
23
- // ---------------------------------------------------------------------------
24
-
25
- /**
26
- * Header name used by the gateway to prove a request originated from it.
27
- * The gateway sends a dedicated gateway-origin secret (or the bearer token
28
- * as fallback). The runtime validates it using constant-time comparison.
29
- * Requests to `/channels/inbound` that lack a valid proof are rejected with 403.
30
- */
31
- export const GATEWAY_ORIGIN_HEADER = 'X-Gateway-Origin';
32
-
33
- /**
34
- * Validate that the request carries a valid gateway-origin proof.
35
- * Uses constant-time comparison to prevent timing attacks.
36
- *
37
- * The `gatewayOriginSecret` parameter is the dedicated secret configured
38
- * via `RUNTIME_GATEWAY_ORIGIN_SECRET`. When set, only this value is
39
- * accepted. When not set, the function falls back to `bearerToken` for
40
- * backward compatibility. When neither is configured (local dev), validation
41
- * is skipped entirely.
42
- */
43
- export function verifyGatewayOrigin(
44
- req: Request,
45
- bearerToken?: string,
46
- gatewayOriginSecret?: string,
47
- ): boolean {
48
- // Determine the expected secret: prefer dedicated secret, fall back to bearer token
49
- const expectedSecret = gatewayOriginSecret ?? bearerToken;
50
- if (!expectedSecret) return true; // No shared secret configured — skip validation
51
- const provided = req.headers.get(GATEWAY_ORIGIN_HEADER);
52
- if (!provided) return false;
53
- const a = Buffer.from(provided);
54
- const b = Buffer.from(expectedSecret);
55
- if (a.length !== b.length) return false;
56
- return timingSafeEqual(a, b);
57
- }
58
-
59
19
  // ---------------------------------------------------------------------------
60
20
  // Actor role
61
21
  // ---------------------------------------------------------------------------
@@ -69,7 +29,13 @@ export const GUARDIAN_APPROVAL_TTL_MS = 30 * 60 * 1000;
69
29
  */
70
30
  export function requiredDecisionKeywords(actions: ApprovalUIMetadata['actions']): string[] {
71
31
  const hasAlways = actions.some((action) => action.id === 'approve_always');
72
- return hasAlways ? ['yes', 'always', 'no'] : ['yes', 'no'];
32
+ const has10m = actions.some((action) => action.id === 'approve_10m');
33
+ const hasThread = actions.some((action) => action.id === 'approve_thread');
34
+ const keywords = ['yes', 'no'];
35
+ if (has10m) keywords.push('approve for 10 minutes');
36
+ if (hasThread) keywords.push('approve for thread');
37
+ if (hasAlways) keywords.push('always');
38
+ return keywords;
73
39
  }
74
40
 
75
41
  // ---------------------------------------------------------------------------
@@ -78,6 +44,8 @@ export function requiredDecisionKeywords(actions: ApprovalUIMetadata['actions'])
78
44
 
79
45
  const VALID_ACTIONS: ReadonlySet<string> = new Set<string>([
80
46
  'approve_once',
47
+ 'approve_10m',
48
+ 'approve_thread',
81
49
  'approve_always',
82
50
  'reject',
83
51
  ]);
@@ -25,7 +25,5 @@ export {
25
25
  _setTestPollMaxWait,
26
26
  type ActorTrustClass,
27
27
  type DenialReason,
28
- GATEWAY_ORIGIN_HEADER,
29
28
  type GuardianContext,
30
- verifyGatewayOrigin,
31
29
  } from './channel-route-shared.js';
@@ -25,7 +25,12 @@ import type { Provider } from '../../providers/types.js';
25
25
  import { getLogger } from '../../util/logger.js';
26
26
  import { buildAssistantEvent } from '../assistant-event.js';
27
27
  import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
28
+ import type { AuthContext } from '../auth/types.js';
28
29
  import { bridgeConfirmationRequestToGuardian } from '../confirmation-request-guardian-bridge.js';
30
+ import {
31
+ resolveGuardianContext,
32
+ toGuardianRuntimeContext,
33
+ } from '../guardian-context-resolver.js';
29
34
  import { routeGuardianReply } from '../guardian-reply-router.js';
30
35
  import { httpError } from '../http-errors.js';
31
36
  import type {
@@ -36,8 +41,6 @@ import type {
36
41
  RuntimeMessagePayload,
37
42
  SendMessageDeps,
38
43
  } from '../http-types.js';
39
- import { resolveLocalIpcGuardianContext } from '../local-actor-identity.js';
40
- import { type ServerWithRequestIP, verifyHttpActorTokenWithLocalFallback } from '../middleware/actor-token.js';
41
44
  import * as pendingInteractions from '../pending-interactions.js';
42
45
 
43
46
  const log = getLogger('conversation-routes');
@@ -338,6 +341,7 @@ function makeHubPublisher(
338
341
  allowlistOptions: msg.allowlistOptions,
339
342
  scopeOptions: msg.scopeOptions,
340
343
  persistentDecisionsAllowed: msg.persistentDecisionsAllowed,
344
+ temporaryOptionsAvailable: msg.temporaryOptionsAvailable,
341
345
  },
342
346
  });
343
347
 
@@ -413,7 +417,7 @@ export async function handleSendMessage(
413
417
  sendMessageDeps?: SendMessageDeps;
414
418
  approvalConversationGenerator?: ApprovalConversationGenerator;
415
419
  },
416
- server: ServerWithRequestIP,
420
+ authContext: AuthContext,
417
421
  ): Promise<Response> {
418
422
  const body = await req.json() as {
419
423
  conversationKey?: string;
@@ -471,35 +475,27 @@ export async function handleSendMessage(
471
475
 
472
476
  // ── Queue-if-busy path (preferred when sendMessageDeps is wired) ────
473
477
  if (deps.sendMessageDeps) {
474
- // Vellum HTTP requests prefer actor-token identity. When absent (e.g. CLI
475
- // bearer-auth only), fall back to local IPC identity resolution so
476
- // bearer-authenticated local clients are not rejected.
477
- const actorVerification = sourceChannel === 'vellum' ? verifyHttpActorTokenWithLocalFallback(req, server) : null;
478
- if (actorVerification && !actorVerification.ok) {
479
- return httpError(
480
- actorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
481
- actorVerification.message,
482
- actorVerification.status,
483
- );
484
- }
485
-
486
478
  const smDeps = deps.sendMessageDeps;
487
479
  const session = await smDeps.getOrCreateSession(mapping.conversationId);
488
- // Resolve actor identity from the verified actor token. The token's
489
- // guardianPrincipalId is matched against the vellum guardian binding
490
- // through the standard trust pipeline.
491
- if (actorVerification?.ok) {
492
- session.setGuardianContext(actorVerification.guardianContext);
480
+
481
+ // Resolve guardian context from the AuthContext's actorPrincipalId.
482
+ // The JWT-verified principal is used as the sender identity through
483
+ // the same trust resolution pipeline that channel ingress uses.
484
+ if (authContext.actorPrincipalId) {
485
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
486
+ const guardianCtx = resolveGuardianContext({
487
+ assistantId,
488
+ sourceChannel: 'vellum',
489
+ conversationExternalId: 'local',
490
+ actorExternalId: authContext.actorPrincipalId,
491
+ });
492
+ session.setGuardianContext(toGuardianRuntimeContext(sourceChannel, guardianCtx));
493
493
  } else {
494
- // Non-vellum channels through the HTTP API are still local
495
- // authenticated requests. Resolve guardian context via the local
496
- // identity pathway (vellum binding lookup) to preserve guardian
497
- // trust. Falls back to a minimal guardian context if no binding
498
- // exists (pre-bootstrap).
499
- session.setGuardianContext(
500
- resolveLocalIpcGuardianContext(sourceChannel) ?? { trustClass: 'guardian', sourceChannel },
501
- );
494
+ // Service principals (svc_gateway) or tokens without an actor ID
495
+ // get a minimal guardian context so downstream code has something.
496
+ session.setGuardianContext({ trustClass: 'guardian', sourceChannel });
502
497
  }
498
+
503
499
  const onEvent = makeHubPublisher(smDeps, mapping.conversationId, session);
504
500
  // Route server-authoritative state signals (confirmation_state_changed,
505
501
  // assistant_activity_state) to the SSE hub. Without this, these signals
@@ -512,14 +508,9 @@ export async function handleSendMessage(
512
508
  : [];
513
509
 
514
510
  // Resolve the verified actor's external user ID and principal for inline
515
- // approval routing. Uses the guardianExternalUserId and guardianPrincipalId
516
- // from the verified context (actor-token or local-fallback).
517
- const verifiedActorExternalUserId = actorVerification?.ok
518
- ? actorVerification.guardianContext.guardianExternalUserId
519
- : session.guardianContext?.guardianExternalUserId;
520
- const verifiedActorPrincipalId = actorVerification?.ok
521
- ? actorVerification.guardianContext.guardianPrincipalId ?? undefined
522
- : session.guardianContext?.guardianPrincipalId ?? undefined;
511
+ // approval routing from the session's guardian context.
512
+ const verifiedActorExternalUserId = session.guardianContext?.guardianExternalUserId;
513
+ const verifiedActorPrincipalId = session.guardianContext?.guardianPrincipalId ?? undefined;
523
514
 
524
515
  // Try to consume the message as a canonical guardian approval/rejection reply.
525
516
  // On failure, degrade to the existing queue/auto-deny path rather than
@@ -617,21 +608,20 @@ export async function handleSendMessage(
617
608
  return httpError('SERVICE_UNAVAILABLE', 'Message processing not configured', 503);
618
609
  }
619
610
 
620
- // Require actor token for vellum channel requests on the legacy path too,
621
- // with local IPC fallback for bearer-authenticated CLI clients.
622
- const legacyActorVerification = sourceChannel === 'vellum' ? verifyHttpActorTokenWithLocalFallback(req, server) : null;
623
- if (legacyActorVerification && !legacyActorVerification.ok) {
624
- return httpError(
625
- legacyActorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
626
- legacyActorVerification.message,
627
- legacyActorVerification.status,
628
- );
611
+ // Resolve guardian context from AuthContext for the legacy path too.
612
+ let guardianContext: import('../../daemon/session-runtime-assembly.js').GuardianRuntimeContext;
613
+ if (authContext.actorPrincipalId) {
614
+ const legacyGuardianCtx = resolveGuardianContext({
615
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
616
+ sourceChannel: 'vellum',
617
+ conversationExternalId: 'local',
618
+ actorExternalId: authContext.actorPrincipalId,
619
+ });
620
+ guardianContext = toGuardianRuntimeContext(sourceChannel, legacyGuardianCtx);
621
+ } else {
622
+ guardianContext = { trustClass: 'guardian' as const, sourceChannel };
629
623
  }
630
624
 
631
- const guardianContext = legacyActorVerification?.ok
632
- ? legacyActorVerification.guardianContext
633
- : resolveLocalIpcGuardianContext(sourceChannel) ?? { trustClass: 'guardian' as const, sourceChannel };
634
-
635
625
  try {
636
626
  const result = await processor(
637
627
  mapping.conversationId,
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * GET /v1/events?conversationKey=...
5
5
  *
6
- * Bearer auth is enforced by RuntimeHttpServer before this handler is called.
7
- * Actor-token identity verification (with local CLI fallback) is performed
8
- * within this handler to bind the SSE stream to a verified actor identity.
6
+ * JWT bearer auth is enforced by RuntimeHttpServer before this handler
7
+ * is called. The AuthContext is threaded through from the HTTP server
8
+ * layer, so no additional actor-token verification is needed here.
9
+ *
9
10
  * Subscribers receive all assistant events scoped to the given conversation.
10
11
  */
11
12
 
@@ -14,8 +15,8 @@ import { formatSseFrame, formatSseHeartbeat } from '../assistant-event.js';
14
15
  import type { AssistantEventSubscription } from '../assistant-event-hub.js';
15
16
  import { AssistantEventHub,assistantEventHub } from '../assistant-event-hub.js';
16
17
  import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
18
+ import type { AuthContext } from '../auth/types.js';
17
19
  import { httpError } from '../http-errors.js';
18
- import { type ServerWithRequestIP, verifyHttpActorTokenWithLocalFallback } from '../middleware/actor-token.js';
19
20
 
20
21
  /** Keep-alive comment sent to idle clients every 30 s by default. */
21
22
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
@@ -24,31 +25,23 @@ const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
24
25
  * Stream assistant events as Server-Sent Events for a specific conversation.
25
26
  *
26
27
  * Query params:
27
- * conversationKey required; scopes the stream to one conversation.
28
+ * conversationKey -- required; scopes the stream to one conversation.
28
29
  *
29
30
  * Options (for testing):
30
- * hub override the event hub (defaults to process singleton).
31
- * heartbeatIntervalMs how often to emit keep-alive comments (default 30 s).
31
+ * hub -- override the event hub (defaults to process singleton).
32
+ * heartbeatIntervalMs -- how often to emit keep-alive comments (default 30 s).
32
33
  */
33
34
  export function handleSubscribeAssistantEvents(
34
35
  req: Request,
35
36
  url: URL,
36
37
  options?:
37
- | { hub?: AssistantEventHub; heartbeatIntervalMs?: number; skipActorVerification?: false; server: ServerWithRequestIP }
38
+ | { hub?: AssistantEventHub; heartbeatIntervalMs?: number; authContext: AuthContext }
38
39
  | { hub?: AssistantEventHub; heartbeatIntervalMs?: number; skipActorVerification: true },
39
40
  ): Response {
40
- // Verify actor-token identity for vellum channel requests, with local
41
- // CLI fallback for bearer-authenticated clients without X-Actor-Token.
42
- if (options && !options.skipActorVerification) {
43
- const actorVerification = verifyHttpActorTokenWithLocalFallback(req, options.server);
44
- if (!actorVerification.ok) {
45
- return httpError(
46
- actorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
47
- actorVerification.message,
48
- actorVerification.status,
49
- );
50
- }
51
- }
41
+ // Auth is already verified upstream by JWT middleware. The AuthContext
42
+ // is available via options.authContext but we don't need to check it
43
+ // further here -- the route policy in http-server.ts already enforced
44
+ // scope and principal type requirements.
52
45
 
53
46
  const conversationKey = url.searchParams.get('conversationKey');
54
47
  if (!conversationKey) {
@@ -61,7 +54,7 @@ export function handleSubscribeAssistantEvents(
61
54
  const mapping = getOrCreateConversation(conversationKey);
62
55
  const encoder = new TextEncoder();
63
56
 
64
- // ── Eager subscribe ──────────────────────────────────────────────────────
57
+ // -- Eager subscribe --------------------------------------------------------
65
58
  // Subscribe before creating the ReadableStream so the callback and onEvict
66
59
  // closures are in place before events can arrive. `controllerRef` is set
67
60
  // synchronously inside ReadableStream's start(), so it is non-null by the
@@ -116,7 +109,7 @@ export function handleSubscribeAssistantEvents(
116
109
  controllerRef = controller;
117
110
 
118
111
  // If the client already disconnected before start() ran, clean up
119
- // immediately the abort event fires once and won't be re-dispatched.
112
+ // immediately -- the abort event fires once and won't be re-dispatched.
120
113
  if (req.signal.aborted) {
121
114
  sub.dispose();
122
115
  cleanup();