@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,28 +4,29 @@
4
4
  * These endpoints let desktop clients fetch pending guardian prompts and
5
5
  * submit button decisions without relying on text parsing.
6
6
  *
7
- * All guardian action endpoints require a valid actor token via the
8
- * X-Actor-Token header (with local CLI fallback). Guardian decisions
9
- * additionally verify the actor is the bound guardian.
7
+ * All guardian action endpoints require a valid JWT bearer token.
8
+ * Auth is verified upstream by JWT middleware; the AuthContext is
9
+ * threaded through from the HTTP server layer.
10
+ *
11
+ * Guardian decisions additionally verify the actor is the bound guardian
12
+ * via the AuthContext's actorPrincipalId.
10
13
  */
11
14
  import {
12
15
  applyCanonicalGuardianDecision,
13
16
  } from '../../approvals/guardian-decision-primitive.js';
17
+ import { isHttpAuthDisabled } from '../../config/env.js';
14
18
  import {
15
19
  type CanonicalGuardianRequest,
16
20
  getCanonicalGuardianRequest,
17
21
  listCanonicalGuardianRequests,
18
22
  } from '../../memory/canonical-guardian-store.js';
23
+ import { getActiveBinding } from '../../memory/guardian-bindings.js';
24
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
25
+ import type { AuthContext } from '../auth/types.js';
19
26
  import type { ApprovalAction } from '../channel-approval-types.js';
20
27
  import type { GuardianDecisionPrompt } from '../guardian-decision-types.js';
21
28
  import { buildDecisionActions } from '../guardian-decision-types.js';
22
29
  import { httpError } from '../http-errors.js';
23
- import {
24
- isActorBoundGuardian,
25
- isLocalFallbackBoundGuardian,
26
- type ServerWithRequestIP,
27
- verifyHttpActorTokenWithLocalFallback,
28
- } from '../middleware/actor-token.js';
29
30
 
30
31
  // ---------------------------------------------------------------------------
31
32
  // GET /v1/guardian-actions/pending?conversationId=...
@@ -33,23 +34,13 @@ import {
33
34
 
34
35
  /**
35
36
  * List pending guardian decision prompts for a conversation.
36
- * Requires a valid actor token.
37
+ * Auth is verified upstream by JWT middleware.
37
38
  *
38
39
  * Returns guardian approval requests (from the channel guardian store) that
39
40
  * are still pending, mapped to the GuardianDecisionPrompt shape so clients
40
41
  * can render structured button UIs.
41
42
  */
42
- export function handleGuardianActionsPending(req: Request, server: ServerWithRequestIP): Response {
43
- const tokenResult = verifyHttpActorTokenWithLocalFallback(req, server);
44
- if (!tokenResult.ok) {
45
- return httpError(
46
- tokenResult.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
47
- tokenResult.message,
48
- tokenResult.status,
49
- );
50
- }
51
-
52
- const url = new URL(req.url);
43
+ export function handleGuardianActionsPending(url: URL, _authContext: AuthContext): Response {
53
44
  const conversationId = url.searchParams.get('conversationId');
54
45
 
55
46
  if (!conversationId) {
@@ -64,29 +55,41 @@ export function handleGuardianActionsPending(req: Request, server: ServerWithReq
64
55
  // POST /v1/guardian-actions/decision
65
56
  // ---------------------------------------------------------------------------
66
57
 
58
+ /**
59
+ * Verify that the actor from AuthContext is the bound guardian for the
60
+ * vellum channel. Returns an error Response if not, or null if allowed.
61
+ */
62
+ function requireBoundGuardian(authContext: AuthContext): Response | null {
63
+ // Dev bypass: when auth is disabled, skip guardian binding check
64
+ // (mirrors enforcePolicy dev bypass in route-policy.ts)
65
+ if (isHttpAuthDisabled()) {
66
+ return null;
67
+ }
68
+ if (!authContext.actorPrincipalId) {
69
+ return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
70
+ }
71
+ const binding = getActiveBinding(DAEMON_INTERNAL_ASSISTANT_ID, 'vellum');
72
+ if (!binding) {
73
+ // No binding yet -- in pre-bootstrap state, allow through
74
+ return null;
75
+ }
76
+ if (binding.guardianExternalUserId !== authContext.actorPrincipalId) {
77
+ return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
78
+ }
79
+ return null;
80
+ }
81
+
67
82
  /**
68
83
  * Submit a guardian action decision.
69
- * Requires a valid actor token for a bound guardian.
84
+ * Requires AuthContext with a bound guardian actor.
70
85
  *
71
86
  * Routes all decisions through the unified canonical guardian decision
72
87
  * primitive which handles CAS resolution, resolver dispatch, and grant
73
88
  * minting.
74
89
  */
75
- export async function handleGuardianActionDecision(req: Request, server: ServerWithRequestIP): Promise<Response> {
76
- const tokenResult = verifyHttpActorTokenWithLocalFallback(req, server);
77
- if (!tokenResult.ok) {
78
- return httpError(
79
- tokenResult.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
80
- tokenResult.message,
81
- tokenResult.status,
82
- );
83
- }
84
- const isBoundGuardian = tokenResult.claims
85
- ? isActorBoundGuardian(tokenResult.claims)
86
- : isLocalFallbackBoundGuardian();
87
- if (!isBoundGuardian) {
88
- return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
89
- }
90
+ export async function handleGuardianActionDecision(req: Request, authContext: AuthContext): Promise<Response> {
91
+ const guardianError = requireBoundGuardian(authContext);
92
+ if (guardianError) return guardianError;
90
93
 
91
94
  const body = await req.json() as {
92
95
  requestId?: string;
@@ -104,9 +107,9 @@ export async function handleGuardianActionDecision(req: Request, server: ServerW
104
107
  return httpError('BAD_REQUEST', 'action is required', 400);
105
108
  }
106
109
 
107
- const VALID_ACTIONS = new Set<string>(['approve_once', 'approve_always', 'reject']);
110
+ const VALID_ACTIONS = new Set<string>(['approve_once', 'approve_10m', 'approve_thread', 'approve_always', 'reject']);
108
111
  if (!VALID_ACTIONS.has(action)) {
109
- return httpError('BAD_REQUEST', `Invalid action: ${action}. Must be one of: approve_once, approve_always, reject`, 400);
112
+ return httpError('BAD_REQUEST', `Invalid action: ${action}. Must be one of: approve_once, approve_10m, approve_thread, approve_always, reject`, 400);
110
113
  }
111
114
 
112
115
  // Verify conversationId scoping before applying the canonical decision.
@@ -118,17 +121,9 @@ export async function handleGuardianActionDecision(req: Request, server: ServerW
118
121
  }
119
122
  }
120
123
 
121
- // Resolve the actor's external user ID: from the token claims if present,
122
- // otherwise from the vellum guardian binding (local fallback).
123
- const actorExternalUserId = tokenResult.claims
124
- ? tokenResult.claims.guardianPrincipalId
125
- : tokenResult.guardianContext.guardianExternalUserId;
126
-
127
- // Resolve the actor's principal ID: from the token claims if present,
128
- // otherwise from the vellum guardian binding (local fallback).
129
- const actorPrincipalId = tokenResult.claims
130
- ? tokenResult.claims.guardianPrincipalId
131
- : tokenResult.guardianContext.guardianPrincipalId ?? undefined;
124
+ // Resolve actor identity from the AuthContext (set by JWT middleware).
125
+ const actorExternalUserId = authContext.actorPrincipalId ?? undefined;
126
+ const actorPrincipalId = authContext.actorPrincipalId ?? undefined;
132
127
 
133
128
  const canonicalResult = await applyCanonicalGuardianDecision({
134
129
  requestId,
@@ -223,7 +218,7 @@ function mapCanonicalRequestToPrompt(
223
218
  ?? (req.toolName ? `Approve tool: ${req.toolName}` : `Guardian request: ${req.kind}`);
224
219
 
225
220
  // pending_question requests are typically voice-originated and need
226
- // approve/reject only (no approve_always guardian-on-behalf invariant).
221
+ // approve/reject only (no approve_always -- guardian-on-behalf invariant).
227
222
  const actions = buildDecisionActions({ forGuardianOnBehalf: true });
228
223
 
229
224
  const expiresAt = req.expiresAt
@@ -18,6 +18,7 @@ import { runApprovalConversationTurn } from '../approval-conversation-turn.js';
18
18
  import { composeApprovalMessageGenerative } from '../approval-message-composer.js';
19
19
  import { parseApprovalDecision } from '../channel-approval-parser.js';
20
20
  import type {
21
+ ApprovalAction,
21
22
  ApprovalDecisionResult,
22
23
  } from '../channel-approval-types.js';
23
24
  import {
@@ -275,12 +276,12 @@ export async function handleApprovalInterception(
275
276
  }
276
277
 
277
278
  // Decision-bearing disposition from the engine
278
- let decisionAction = engineResult.disposition as 'approve_once' | 'approve_always' | 'reject';
279
+ let decisionAction = engineResult.disposition as ApprovalAction;
279
280
 
280
- // Belt-and-suspenders: guardians cannot approve_always even if the
281
- // engine returns it (the engine's allowedActions validation should
281
+ // Belt-and-suspenders: guardians cannot use broad allow modes even if
282
+ // the engine returns them (the engine's allowedActions validation should
282
283
  // already prevent this, but enforce it here too).
283
- if (decisionAction === 'approve_always') {
284
+ if (decisionAction === 'approve_always' || decisionAction === 'approve_10m' || decisionAction === 'approve_thread') {
284
285
  decisionAction = 'approve_once';
285
286
  }
286
287
 
@@ -838,7 +839,7 @@ export async function handleApprovalInterception(
838
839
  }
839
840
 
840
841
  // Decision-bearing disposition — map to ApprovalDecisionResult and apply
841
- const decisionAction = engineResult.disposition as 'approve_once' | 'approve_always' | 'reject';
842
+ const decisionAction = engineResult.disposition as ApprovalAction;
842
843
  const engineDecision: ApprovalDecisionResult = {
843
844
  action: decisionAction,
844
845
  source: 'plain_text',
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Idempotent bootstrap endpoint for the vellum guardian channel.
5
5
  * Creates or confirms a guardianPrincipalId and channel='vellum'
6
- * guardian binding, then mints and returns an actor token bound
7
- * to (assistantId, guardianPrincipalId, deviceId).
6
+ * guardian binding, then mints and returns a JWT access token bound
7
+ * to (assistantId, guardianPrincipalId) with a paired refresh token.
8
8
  *
9
- * Only the hashed token is persisted.
9
+ * Only the hashed tokens are persisted.
10
10
  */
11
11
 
12
12
  import { createHash } from 'node:crypto';
@@ -18,10 +18,14 @@ import {
18
18
  getActiveBinding,
19
19
  } from '../../memory/guardian-bindings.js';
20
20
  import { getLogger } from '../../util/logger.js';
21
- import { mintCredentialPair } from '../actor-refresh-token-service.js';
22
21
  import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
22
+ import { mintCredentialPair } from '../auth/credential-service.js';
23
23
  import { httpError } from '../http-errors.js';
24
- import type { ServerWithRequestIP } from '../middleware/actor-token.js';
24
+
25
+ /** Bun server shape needed for requestIP -- avoids importing the full Bun type. */
26
+ type ServerWithRequestIP = {
27
+ requestIP(req: Request): { address: string; family: string; port: number } | null;
28
+ };
25
29
 
26
30
  const log = getLogger('guardian-bootstrap');
27
31
 
@@ -68,7 +72,7 @@ const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
68
72
  * Handle POST /v1/integrations/guardian/vellum/bootstrap
69
73
  *
70
74
  * Body: { platform: 'macos', deviceId: string }
71
- * Returns: { guardianPrincipalId, actorToken, isNew }
75
+ * Returns: { guardianPrincipalId, accessToken, isNew }
72
76
  *
73
77
  * This endpoint is loopback-only (macOS local use only). iOS devices
74
78
  * obtain actor tokens exclusively through the QR pairing flow.
@@ -118,8 +122,8 @@ export async function handleGuardianBootstrap(req: Request, server: ServerWithRe
118
122
 
119
123
  return Response.json({
120
124
  guardianPrincipalId,
121
- actorToken: credentials.actorToken,
122
- actorTokenExpiresAt: credentials.actorTokenExpiresAt,
125
+ accessToken: credentials.accessToken,
126
+ accessTokenExpiresAt: credentials.accessTokenExpiresAt,
123
127
  refreshToken: credentials.refreshToken,
124
128
  refreshTokenExpiresAt: credentials.refreshTokenExpiresAt,
125
129
  refreshAfter: credentials.refreshAfter,
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { getLogger } from '../../util/logger.js';
9
- import { rotateCredentials } from '../actor-refresh-token-service.js';
9
+ import { rotateCredentials } from '../auth/credential-service.js';
10
10
  import { httpError } from '../http-errors.js';
11
11
 
12
12
  const log = getLogger('guardian-refresh');
@@ -15,7 +15,7 @@ const log = getLogger('guardian-refresh');
15
15
  * Handle POST /v1/integrations/guardian/vellum/refresh
16
16
  *
17
17
  * Body: { platform: 'ios' | 'macos', deviceId: string, refreshToken: string }
18
- * Returns: { guardianPrincipalId, actorToken, actorTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter }
18
+ * Returns: { guardianPrincipalId, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter }
19
19
  */
20
20
  export async function handleGuardianRefresh(req: Request): Promise<Response> {
21
21
  try {
@@ -26,9 +26,9 @@ import { checkIngressForSecrets } from '../../security/secret-ingress.js';
26
26
  import { canonicalizeInboundIdentity } from '../../util/canonicalize-identity.js';
27
27
  import { IngressBlockedError } from '../../util/errors.js';
28
28
  import { getLogger } from '../../util/logger.js';
29
- import { readHttpToken } from '../../util/platform.js';
30
29
  import { notifyGuardianOfAccessRequest } from '../access-request-helper.js';
31
30
  import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
31
+ import { mintDaemonDeliveryToken } from '../auth/token-service.js';
32
32
  import {
33
33
  buildApprovalUIMetadata,
34
34
  getApprovalInfoByConversation,
@@ -70,7 +70,6 @@ import {
70
70
  GUARDIAN_APPROVAL_TTL_MS,
71
71
  type GuardianContext,
72
72
  stripVerificationFailurePrefix,
73
- verifyGatewayOrigin,
74
73
  } from './channel-route-shared.js';
75
74
  import { handleApprovalInterception } from './guardian-approval-interception.js';
76
75
  import { deliverGeneratedApprovalPrompt } from './guardian-approval-prompt.js';
@@ -96,24 +95,22 @@ function parseGuardianVerifyCode(content: string): string | undefined {
96
95
  export async function handleChannelInbound(
97
96
  req: Request,
98
97
  processMessage?: MessageProcessor,
99
- bearerToken?: string,
100
98
  assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID,
101
- gatewayOriginSecret?: string,
102
99
  approvalCopyGenerator?: ApprovalCopyGenerator,
103
100
  approvalConversationGenerator?: ApprovalConversationGenerator,
104
101
  _guardianActionCopyGenerator?: GuardianActionCopyGenerator,
105
102
  _guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator,
106
103
  ): Promise<Response> {
107
- // Reject requests that lack valid gateway-origin proof. This ensures
108
- // channel inbound messages can only arrive via the gateway (which
109
- // performs webhook-level verification) and not via direct HTTP calls.
110
- if (!verifyGatewayOrigin(req, bearerToken, gatewayOriginSecret)) {
111
- log.warn('Rejected channel inbound request: missing or invalid gateway-origin proof');
112
- return Response.json(
113
- { error: 'Forbidden: missing gateway-origin proof', code: 'GATEWAY_ORIGIN_REQUIRED' },
114
- { status: 403 },
115
- );
116
- }
104
+ // Gateway-origin proof is enforced by route-policy middleware (svc_gateway
105
+ // principal type required) before this handler runs. The exchange JWT
106
+ // itself proves gateway origin.
107
+
108
+ // Factory that mints a fresh short-lived JWT for each daemon-to-gateway
109
+ // delivery callback. The JWT has a 60-second TTL, so long-running
110
+ // background operations (typing heartbeats, approval watchers, reply
111
+ // delivery) must call this at each delivery attempt rather than reusing
112
+ // a single token from request start.
113
+ const mintBearerToken = (): string => mintDaemonDeliveryToken();
117
114
 
118
115
  const body = await req.json() as {
119
116
  sourceChannel?: string;
@@ -308,7 +305,7 @@ export async function handleChannelInbound(
308
305
  senderName: body.actorDisplayName,
309
306
  senderUsername: body.actorUsername,
310
307
  replyCallbackUrl: body.replyCallbackUrl,
311
- bearerToken,
308
+ bearerToken: mintBearerToken(),
312
309
  assistantId,
313
310
  canonicalAssistantId,
314
311
  });
@@ -345,7 +342,7 @@ export async function handleChannelInbound(
345
342
  chatId: conversationExternalId,
346
343
  text: replyText,
347
344
  assistantId,
348
- }, bearerToken);
345
+ }, mintBearerToken());
349
346
  } catch (err) {
350
347
  log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
351
348
  }
@@ -394,7 +391,7 @@ export async function handleChannelInbound(
394
391
  senderName: body.actorDisplayName,
395
392
  senderUsername: body.actorUsername,
396
393
  replyCallbackUrl: body.replyCallbackUrl,
397
- bearerToken,
394
+ bearerToken: mintBearerToken(),
398
395
  assistantId,
399
396
  canonicalAssistantId,
400
397
  });
@@ -434,7 +431,7 @@ export async function handleChannelInbound(
434
431
  chatId: conversationExternalId,
435
432
  text: replyText,
436
433
  assistantId,
437
- }, bearerToken);
434
+ }, mintBearerToken());
438
435
  } catch (err) {
439
436
  log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
440
437
  }
@@ -451,7 +448,7 @@ export async function handleChannelInbound(
451
448
  chatId: conversationExternalId,
452
449
  text: "Sorry, you haven't been approved to message this assistant.",
453
450
  assistantId,
454
- }, bearerToken);
451
+ }, mintBearerToken());
455
452
  } catch (err) {
456
453
  log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
457
454
  }
@@ -567,7 +564,7 @@ export async function handleChannelInbound(
567
564
  chatId: pendingReply.chatId,
568
565
  text: pendingReply.text,
569
566
  assistantId: pendingReply.assistantId,
570
- }, bearerToken);
567
+ }, mintBearerToken());
571
568
  channelDeliveryStore.clearPendingVerificationReply(result.eventId);
572
569
  log.info({ eventId: result.eventId }, 'Retried pending verification reply: delivered');
573
570
  } catch (retryErr) {
@@ -864,7 +861,7 @@ export async function handleChannelInbound(
864
861
  chatId: conversationExternalId,
865
862
  text: replyText,
866
863
  assistantId,
867
- }, bearerToken);
864
+ }, mintBearerToken());
868
865
  } catch (err) {
869
866
  // The challenge is already consumed and side effects applied, so
870
867
  // we cannot simply re-throw and let the gateway retry the full
@@ -887,7 +884,7 @@ export async function handleChannelInbound(
887
884
  chatId: conversationExternalId,
888
885
  text: replyText,
889
886
  assistantId,
890
- }, bearerToken);
887
+ }, mintBearerToken());
891
888
  log.info({ eventId: result.eventId }, 'Verification reply delivered on self-retry');
892
889
  channelDeliveryStore.clearPendingVerificationReply(result.eventId);
893
890
  } catch (retryErr) {
@@ -992,7 +989,7 @@ export async function handleChannelInbound(
992
989
  replyCallbackUrl,
993
990
  guardianChatId: conversationExternalId,
994
991
  assistantId: canonicalAssistantId,
995
- bearerToken,
992
+ bearerToken: mintBearerToken(),
996
993
  },
997
994
  });
998
995
 
@@ -1004,7 +1001,7 @@ export async function handleChannelInbound(
1004
1001
  chatId: conversationExternalId,
1005
1002
  text: routerResult.replyText,
1006
1003
  assistantId: canonicalAssistantId,
1007
- }, bearerToken);
1004
+ }, mintBearerToken());
1008
1005
  } catch (err) {
1009
1006
  log.error({ err, conversationExternalId }, 'Failed to deliver canonical router reply');
1010
1007
  }
@@ -1042,7 +1039,7 @@ export async function handleChannelInbound(
1042
1039
  sourceChannel,
1043
1040
  actorExternalId: canonicalSenderId ?? rawSenderId,
1044
1041
  replyCallbackUrl,
1045
- bearerToken,
1042
+ bearerToken: mintBearerToken(),
1046
1043
  guardianCtx,
1047
1044
  assistantId: canonicalAssistantId,
1048
1045
  approvalCopyGenerator,
@@ -1200,7 +1197,7 @@ export async function handleChannelInbound(
1200
1197
  commandIntent,
1201
1198
  sourceLanguageCode,
1202
1199
  replyCallbackUrl,
1203
- bearerToken,
1200
+ mintBearerToken,
1204
1201
  assistantId: canonicalAssistantId,
1205
1202
  approvalCopyGenerator,
1206
1203
  });
@@ -1348,7 +1345,8 @@ interface BackgroundProcessingParams {
1348
1345
  metadataHints: string[];
1349
1346
  metadataUxBrief?: string;
1350
1347
  replyCallbackUrl?: string;
1351
- bearerToken?: string;
1348
+ /** Factory that mints a fresh delivery JWT for each HTTP attempt. */
1349
+ mintBearerToken: () => string;
1352
1350
  assistantId?: string;
1353
1351
  approvalCopyGenerator?: ApprovalCopyGenerator;
1354
1352
  commandIntent?: Record<string, unknown>;
@@ -1384,7 +1382,7 @@ function shouldEmitTelegramTyping(
1384
1382
  function startTelegramTypingHeartbeat(
1385
1383
  callbackUrl: string,
1386
1384
  chatId: string,
1387
- bearerToken?: string,
1385
+ mintBearerToken: () => string,
1388
1386
  assistantId?: string,
1389
1387
  ): () => void {
1390
1388
  let active = true;
@@ -1396,7 +1394,7 @@ function startTelegramTypingHeartbeat(
1396
1394
  void deliverChannelReply(
1397
1395
  callbackUrl,
1398
1396
  { chatId, chatAction: 'typing', assistantId },
1399
- bearerToken,
1397
+ mintBearerToken(),
1400
1398
  ).catch((err) => {
1401
1399
  log.debug({ err, chatId }, 'Failed to deliver Telegram typing indicator');
1402
1400
  }).finally(() => {
@@ -1423,7 +1421,7 @@ function startPendingApprovalPromptWatcher(params: {
1423
1421
  guardianExternalUserId?: string;
1424
1422
  requesterExternalUserId?: string;
1425
1423
  replyCallbackUrl: string;
1426
- bearerToken?: string;
1424
+ mintBearerToken: () => string;
1427
1425
  assistantId?: string;
1428
1426
  approvalCopyGenerator?: ApprovalCopyGenerator;
1429
1427
  }): () => void {
@@ -1435,7 +1433,7 @@ function startPendingApprovalPromptWatcher(params: {
1435
1433
  guardianExternalUserId,
1436
1434
  requesterExternalUserId,
1437
1435
  replyCallbackUrl,
1438
- bearerToken,
1436
+ mintBearerToken,
1439
1437
  assistantId,
1440
1438
  approvalCopyGenerator,
1441
1439
  } = params;
@@ -1467,7 +1465,7 @@ function startPendingApprovalPromptWatcher(params: {
1467
1465
  chatId: externalChatId,
1468
1466
  sourceChannel,
1469
1467
  assistantId: assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
1470
- bearerToken,
1468
+ bearerToken: mintBearerToken(),
1471
1469
  prompt,
1472
1470
  uiMetadata: buildApprovalUIMetadata(prompt, info),
1473
1471
  messageContext: {
@@ -1535,7 +1533,7 @@ function startTrustedContactApprovalNotifier(params: {
1535
1533
  guardianTrustClass: GuardianContext['trustClass'];
1536
1534
  guardianExternalUserId?: string;
1537
1535
  replyCallbackUrl: string;
1538
- bearerToken?: string;
1536
+ mintBearerToken: () => string;
1539
1537
  assistantId?: string;
1540
1538
  }): () => void {
1541
1539
  const {
@@ -1545,7 +1543,7 @@ function startTrustedContactApprovalNotifier(params: {
1545
1543
  guardianTrustClass,
1546
1544
  guardianExternalUserId,
1547
1545
  replyCallbackUrl,
1548
- bearerToken,
1546
+ mintBearerToken,
1549
1547
  assistantId,
1550
1548
  } = params;
1551
1549
 
@@ -1588,7 +1586,7 @@ function startTrustedContactApprovalNotifier(params: {
1588
1586
  chatId: externalChatId,
1589
1587
  text: waitingText,
1590
1588
  assistantId: assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
1591
- }, bearerToken);
1589
+ }, mintBearerToken());
1592
1590
  } catch (err) {
1593
1591
  log.warn({ err, conversationId }, 'Failed to deliver trusted-contact pending-approval notification');
1594
1592
  // Remove from notified set so delivery is retried on next poll
@@ -1630,7 +1628,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
1630
1628
  metadataHints,
1631
1629
  metadataUxBrief,
1632
1630
  replyCallbackUrl,
1633
- bearerToken,
1631
+ mintBearerToken,
1634
1632
  assistantId,
1635
1633
  approvalCopyGenerator,
1636
1634
  commandIntent,
@@ -1642,7 +1640,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
1642
1640
  ? replyCallbackUrl
1643
1641
  : undefined;
1644
1642
  const stopTypingHeartbeat = typingCallbackUrl
1645
- ? startTelegramTypingHeartbeat(typingCallbackUrl, externalChatId, bearerToken, assistantId)
1643
+ ? startTelegramTypingHeartbeat(typingCallbackUrl, externalChatId, mintBearerToken, assistantId)
1646
1644
  : undefined;
1647
1645
  const stopApprovalWatcher = replyCallbackUrl
1648
1646
  ? startPendingApprovalPromptWatcher({
@@ -1653,7 +1651,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
1653
1651
  guardianExternalUserId: guardianCtx.guardianExternalUserId,
1654
1652
  requesterExternalUserId: guardianCtx.requesterExternalUserId,
1655
1653
  replyCallbackUrl,
1656
- bearerToken,
1654
+ mintBearerToken,
1657
1655
  assistantId,
1658
1656
  approvalCopyGenerator,
1659
1657
  })
@@ -1666,7 +1664,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
1666
1664
  guardianTrustClass: guardianCtx.trustClass,
1667
1665
  guardianExternalUserId: guardianCtx.guardianExternalUserId,
1668
1666
  replyCallbackUrl,
1669
- bearerToken,
1667
+ mintBearerToken,
1670
1668
  assistantId,
1671
1669
  })
1672
1670
  : undefined;
@@ -1701,7 +1699,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
1701
1699
  conversationId,
1702
1700
  externalChatId,
1703
1701
  replyCallbackUrl,
1704
- bearerToken,
1702
+ mintBearerToken(),
1705
1703
  assistantId,
1706
1704
  {
1707
1705
  onSegmentDelivered: (count) =>
@@ -1735,11 +1733,7 @@ function deliverBootstrapVerificationTelegram(
1735
1733
  ): void {
1736
1734
  const attemptDelivery = async (): Promise<boolean> => {
1737
1735
  const gatewayUrl = getGatewayInternalBaseUrl();
1738
- const bearerToken = readHttpToken();
1739
- if (!bearerToken) {
1740
- log.error('Cannot deliver bootstrap verification Telegram message: no runtime HTTP token available');
1741
- return false;
1742
- }
1736
+ const bearerToken = mintDaemonDeliveryToken();
1743
1737
  const url = `${gatewayUrl}/deliver/telegram`;
1744
1738
  const resp = await fetch(url, {
1745
1739
  method: 'POST',
@@ -10,16 +10,16 @@ import {
10
10
  import type { ServerMessage } from '../../daemon/ipc-contract.js';
11
11
  import { PairingStore } from '../../daemon/pairing-store.js';
12
12
  import { getLogger } from '../../util/logger.js';
13
- import { mintCredentialPair } from '../actor-refresh-token-service.js';
14
13
  import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
14
+ import { mintCredentialPair } from '../auth/credential-service.js';
15
15
  import { ensureVellumGuardianBinding } from '../guardian-vellum-migration.js';
16
16
  import { httpError } from '../http-errors.js';
17
17
 
18
18
  const log = getLogger('runtime-http');
19
19
 
20
20
  interface PairingCredentials {
21
- actorToken: string;
22
- actorTokenExpiresAt: number;
21
+ accessToken: string;
22
+ accessTokenExpiresAt: number;
23
23
  refreshToken: string;
24
24
  refreshTokenExpiresAt: number;
25
25
  refreshAfter: number;
@@ -50,8 +50,8 @@ function mintPairingCredentials(deviceId: string, platform: string): PairingCred
50
50
 
51
51
  log.info({ assistantId, platform }, 'Minted credentials during pairing');
52
52
  return {
53
- actorToken: credentials.actorToken,
54
- actorTokenExpiresAt: credentials.actorTokenExpiresAt,
53
+ accessToken: credentials.accessToken,
54
+ accessTokenExpiresAt: credentials.accessTokenExpiresAt,
55
55
  refreshToken: credentials.refreshToken,
56
56
  refreshTokenExpiresAt: credentials.refreshTokenExpiresAt,
57
57
  refreshAfter: credentials.refreshAfter,
@@ -213,8 +213,8 @@ export async function handlePairingRequest(req: Request, ctx: PairingHandlerCont
213
213
  localLanUrl: entry.localLanUrl,
214
214
  ...(ctx.featureFlagToken ? { featureFlagToken: ctx.featureFlagToken } : {}),
215
215
  ...(credentials ? {
216
- actorToken: credentials.actorToken,
217
- actorTokenExpiresAt: credentials.actorTokenExpiresAt,
216
+ accessToken: credentials.accessToken,
217
+ accessTokenExpiresAt: credentials.accessTokenExpiresAt,
218
218
  refreshToken: credentials.refreshToken,
219
219
  refreshTokenExpiresAt: credentials.refreshTokenExpiresAt,
220
220
  refreshAfter: credentials.refreshAfter,
@@ -312,8 +312,8 @@ export function handlePairingStatus(url: URL, ctx: PairingHandlerContext): Respo
312
312
  localLanUrl: entry.localLanUrl,
313
313
  ...(ctx.featureFlagToken ? { featureFlagToken: ctx.featureFlagToken } : {}),
314
314
  ...(credentialEntry ? {
315
- actorToken: credentialEntry.credentials.actorToken,
316
- actorTokenExpiresAt: credentialEntry.credentials.actorTokenExpiresAt,
315
+ accessToken: credentialEntry.credentials.accessToken,
316
+ accessTokenExpiresAt: credentialEntry.credentials.accessTokenExpiresAt,
317
317
  refreshToken: credentialEntry.credentials.refreshToken,
318
318
  refreshTokenExpiresAt: credentialEntry.credentials.refreshTokenExpiresAt,
319
319
  refreshAfter: credentialEntry.credentials.refreshAfter,