@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,222 @@
1
+ /**
2
+ * Route handlers for the brain graph visualization endpoint.
3
+ *
4
+ * Queries the memory database to return a knowledge graph shaped for brain-lobe
5
+ * visualization, with entities mapped to brain regions based on their type.
6
+ */
7
+
8
+ import { readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ import { count } from 'drizzle-orm';
12
+
13
+ import { getDb } from '../../memory/db.js';
14
+ import { memoryEntities, memoryEntityRelations, memoryItems } from '../../memory/schema.js';
15
+ import { resolveBundledDir } from '../../util/bundled-asset.js';
16
+
17
+ function getLobeRegion(entityType: string): string {
18
+ switch (entityType) {
19
+ case 'person':
20
+ case 'organization':
21
+ return 'right-social';
22
+ case 'project':
23
+ case 'company':
24
+ return 'left-planning';
25
+ case 'tool':
26
+ return 'left-technical';
27
+ case 'concept':
28
+ return 'right-creative';
29
+ case 'location':
30
+ return 'right-spatial';
31
+ default:
32
+ return 'center';
33
+ }
34
+ }
35
+
36
+ function getEntityColor(entityType: string): string {
37
+ switch (entityType) {
38
+ case 'person':
39
+ return '#22c55e';
40
+ case 'project':
41
+ return '#f97316';
42
+ case 'tool':
43
+ return '#06b6d4';
44
+ case 'company':
45
+ return '#a855f7';
46
+ case 'organization':
47
+ return '#a855f7';
48
+ case 'concept':
49
+ return '#eab308';
50
+ case 'location':
51
+ return '#14b8a6';
52
+ default:
53
+ return '#94a3b8';
54
+ }
55
+ }
56
+
57
+ function getMemoryKindColor(kind: string): string {
58
+ switch (kind) {
59
+ case 'profile':
60
+ return '#8b5cf6';
61
+ case 'preference':
62
+ return '#3b82f6';
63
+ case 'constraint':
64
+ return '#ef4444';
65
+ case 'instruction':
66
+ return '#f59e0b';
67
+ case 'style':
68
+ return '#ec4899';
69
+ default:
70
+ return '#94a3b8';
71
+ }
72
+ }
73
+
74
+ export function handleGetBrainGraph(): Response {
75
+ try {
76
+ const db = getDb();
77
+
78
+ const entityRows = db
79
+ .select({
80
+ id: memoryEntities.id,
81
+ name: memoryEntities.name,
82
+ type: memoryEntities.type,
83
+ mentionCount: memoryEntities.mentionCount,
84
+ firstSeenAt: memoryEntities.firstSeenAt,
85
+ lastSeenAt: memoryEntities.lastSeenAt,
86
+ })
87
+ .from(memoryEntities)
88
+ .all();
89
+
90
+ const relationRows = db
91
+ .select({
92
+ sourceEntityId: memoryEntityRelations.sourceEntityId,
93
+ targetEntityId: memoryEntityRelations.targetEntityId,
94
+ relation: memoryEntityRelations.relation,
95
+ })
96
+ .from(memoryEntityRelations)
97
+ .all();
98
+
99
+ const kindCountRows = db
100
+ .select({
101
+ kind: memoryItems.kind,
102
+ count: count(),
103
+ })
104
+ .from(memoryItems)
105
+ .groupBy(memoryItems.kind)
106
+ .all();
107
+
108
+ const entities = entityRows.map((entity) => ({
109
+ id: entity.id,
110
+ name: entity.name,
111
+ type: entity.type,
112
+ lobeRegion: getLobeRegion(entity.type),
113
+ color: getEntityColor(entity.type),
114
+ mentionCount: entity.mentionCount,
115
+ firstSeenAt: entity.firstSeenAt,
116
+ lastSeenAt: entity.lastSeenAt,
117
+ }));
118
+
119
+ const relations = relationRows.map((rel) => ({
120
+ sourceId: rel.sourceEntityId,
121
+ targetId: rel.targetEntityId,
122
+ relation: rel.relation,
123
+ }));
124
+
125
+ const memorySummary = kindCountRows.map((row) => ({
126
+ kind: row.kind,
127
+ count: row.count,
128
+ color: getMemoryKindColor(row.kind),
129
+ }));
130
+
131
+ const totalKnowledgeCount = memorySummary.reduce((sum, entry) => sum + entry.count, 0);
132
+
133
+ return Response.json({
134
+ entities,
135
+ relations,
136
+ memorySummary,
137
+ totalKnowledgeCount,
138
+ generatedAt: new Date().toISOString(),
139
+ });
140
+ } catch (err) {
141
+ return Response.json(
142
+ { error: 'Failed to generate brain graph', detail: err instanceof Error ? err.message : String(err) },
143
+ { status: 500 },
144
+ );
145
+ }
146
+ }
147
+
148
+ export function handleServeHomeBaseUI(bearerToken?: string): Response {
149
+ try {
150
+ const prebuiltDir = resolveBundledDir(
151
+ import.meta.dirname ?? __dirname,
152
+ '../../home-base/prebuilt',
153
+ 'prebuilt',
154
+ );
155
+ let html = readFileSync(join(prebuiltDir, 'index.html'), 'utf-8');
156
+ if (bearerToken) {
157
+ const escapedToken = bearerToken
158
+ .replace(/&/g, '&')
159
+ .replace(/"/g, '"')
160
+ .replace(/</g, '&lt;')
161
+ .replace(/>/g, '&gt;');
162
+ html = html.replace(
163
+ '</head>',
164
+ ` <meta name="api-token" content="${escapedToken}">\n</head>`,
165
+ );
166
+ }
167
+ return new Response(html, {
168
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
169
+ });
170
+ } catch (err) {
171
+ return Response.json(
172
+ { error: 'Home Base UI not available', detail: err instanceof Error ? err.message : String(err) },
173
+ { status: 500 },
174
+ );
175
+ }
176
+ }
177
+
178
+ export function handleServeBrainGraphUI(bearerToken?: string): Response {
179
+ try {
180
+ const prebuiltDir = resolveBundledDir(
181
+ import.meta.dirname ?? __dirname,
182
+ '../../home-base/prebuilt',
183
+ 'prebuilt',
184
+ );
185
+ let html = readFileSync(join(prebuiltDir, 'brain-graph.html'), 'utf-8');
186
+ if (bearerToken) {
187
+ // Inject token as a meta tag for client-side fetch authentication.
188
+ // HTML-escape the token value to guard against injection if the token
189
+ // comes from an environment variable with special characters.
190
+ const escapedToken = bearerToken
191
+ .replace(/&/g, '&amp;')
192
+ .replace(/"/g, '&quot;')
193
+ .replace(/</g, '&lt;')
194
+ .replace(/>/g, '&gt;');
195
+ html = html.replace(
196
+ '</head>',
197
+ ` <meta name="api-token" content="${escapedToken}">\n</head>`,
198
+ );
199
+ }
200
+ // CSP permits the CDN sources required by D3.js and Three.js.
201
+ // 'unsafe-eval' is needed by Three.js's shader compilation path.
202
+ const csp = [
203
+ "default-src 'self'",
204
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://d3js.org",
205
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
206
+ "font-src 'self' https://fonts.gstatic.com",
207
+ "connect-src 'self'",
208
+ "img-src 'self' data:",
209
+ ].join('; ');
210
+ return new Response(html, {
211
+ headers: {
212
+ 'Content-Type': 'text/html; charset=utf-8',
213
+ 'Content-Security-Policy': csp,
214
+ },
215
+ });
216
+ } catch (err) {
217
+ return Response.json(
218
+ { error: 'Brain graph UI not available', detail: err instanceof Error ? err.message : String(err) },
219
+ { status: 500 },
220
+ );
221
+ }
222
+ }
@@ -11,6 +11,7 @@
11
11
  import { answerCall, cancelCall, getCallStatus, relayInstruction,startCall } from '../../calls/call-domain.js';
12
12
  import { getConfig } from '../../config/loader.js';
13
13
  import { VALID_CALLER_IDENTITY_MODES } from '../../config/schema.js';
14
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
14
15
  import { httpError, httpErrorCodeFromStatus } from '../http-errors.js';
15
16
 
16
17
  // ── Idempotency cache ─────────────────────────────────────────────────────────
@@ -41,7 +42,7 @@ function pruneIdempotencyCache(): void {
41
42
  * Optional `idempotencyKey`: if supplied, duplicate requests with the same key
42
43
  * within 5 minutes return the cached 201 response without starting a second call.
43
44
  */
44
- export async function handleStartCall(req: Request, assistantId: string = 'self'): Promise<Response> {
45
+ export async function handleStartCall(req: Request, assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): Promise<Response> {
45
46
  if (!getConfig().calls.enabled) {
46
47
  return httpError('FORBIDDEN', 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.', 403);
47
48
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Route handlers for channel readiness endpoints.
3
+ *
4
+ * GET /v1/channels/readiness — get channel readiness snapshots
5
+ * POST /v1/channels/readiness/refresh — invalidate cache and refresh readiness
6
+ */
7
+
8
+ import type { ChannelId } from '../../channels/types.js';
9
+ import { getReadinessService } from '../../daemon/handlers/config-channels.js';
10
+
11
+ /**
12
+ * GET /v1/channels/readiness
13
+ *
14
+ * Query params: channel? (optional ChannelId), includeRemote? (optional boolean)
15
+ */
16
+ export async function handleGetChannelReadiness(url: URL): Promise<Response> {
17
+ const channel = (url.searchParams.get('channel') as ChannelId | null) ?? undefined;
18
+ const includeRemote = url.searchParams.get('includeRemote') === 'true';
19
+
20
+ const service = getReadinessService();
21
+ const snapshots = await service.getReadiness(channel, includeRemote);
22
+
23
+ return Response.json({
24
+ success: true,
25
+ snapshots: snapshots.map((s) => ({
26
+ channel: s.channel,
27
+ ready: s.ready,
28
+ checkedAt: s.checkedAt,
29
+ stale: s.stale,
30
+ reasons: s.reasons,
31
+ localChecks: s.localChecks,
32
+ remoteChecks: s.remoteChecks,
33
+ })),
34
+ });
35
+ }
36
+
37
+ /**
38
+ * POST /v1/channels/readiness/refresh
39
+ *
40
+ * Body: { channel?: ChannelId, includeRemote?: boolean }
41
+ */
42
+ export async function handleRefreshChannelReadiness(req: Request): Promise<Response> {
43
+ const body = (await req.json().catch(() => ({}))) as {
44
+ channel?: ChannelId;
45
+ includeRemote?: boolean;
46
+ };
47
+
48
+ const service = getReadinessService();
49
+
50
+ // Invalidate cache before fetching
51
+ if (body.channel) {
52
+ service.invalidateChannel(body.channel);
53
+ } else {
54
+ service.invalidateAll();
55
+ }
56
+
57
+ const snapshots = await service.getReadiness(body.channel, body.includeRemote);
58
+
59
+ return Response.json({
60
+ success: true,
61
+ snapshots: snapshots.map((s) => ({
62
+ channel: s.channel,
63
+ ready: s.ready,
64
+ checkedAt: s.checkedAt,
65
+ stale: s.stale,
66
+ reasons: s.reasons,
67
+ localChecks: s.localChecks,
68
+ remoteChecks: s.remoteChecks,
69
+ })),
70
+ });
71
+ }
@@ -4,7 +4,7 @@
4
4
  import { timingSafeEqual } from 'node:crypto';
5
5
 
6
6
  import type { ChannelId } from '../../channels/types.js';
7
- import { normalizeAssistantId } from '../../util/platform.js';
7
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
8
8
  import type {
9
9
  ApprovalAction,
10
10
  ApprovalDecisionResult,
@@ -15,8 +15,8 @@ export type { ActorTrustClass, DenialReason, GuardianContext } from '../guardian
15
15
  export { toGuardianRuntimeContext } from '../guardian-context-resolver.js';
16
16
 
17
17
  /** Canonicalize assistantId for channel ingress paths. */
18
- export function canonicalChannelAssistantId(assistantId: string): string {
19
- return normalizeAssistantId(assistantId);
18
+ export function canonicalChannelAssistantId(_assistantId: string): string {
19
+ return DAEMON_INTERNAL_ASSISTANT_ID;
20
20
  }
21
21
 
22
22
  // ---------------------------------------------------------------------------
@@ -10,6 +10,7 @@ import {
10
10
  } from '../../memory/conversation-attention-store.js';
11
11
  import * as conversationStore from '../../memory/conversation-store.js';
12
12
  import { truncate } from '../../util/truncate.js';
13
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
13
14
  import { httpError } from '../http-errors.js';
14
15
 
15
16
  export function handleListConversationAttention(url: URL): Response {
@@ -27,7 +28,7 @@ export function handleListConversationAttention(url: URL): Response {
27
28
  }
28
29
 
29
30
  const attentionStates = listConversationAttention({
30
- assistantId: 'self',
31
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
31
32
  state: stateParam as AttentionFilterState,
32
33
  sourceChannel: channel,
33
34
  source: sourceParam !== 'all' ? sourceParam : undefined,
@@ -24,6 +24,8 @@ import { getConfiguredProvider } from '../../providers/provider-send-message.js'
24
24
  import type { Provider } from '../../providers/types.js';
25
25
  import { getLogger } from '../../util/logger.js';
26
26
  import { buildAssistantEvent } from '../assistant-event.js';
27
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
28
+ import { bridgeConfirmationRequestToGuardian } from '../confirmation-request-guardian-bridge.js';
27
29
  import { routeGuardianReply } from '../guardian-reply-router.js';
28
30
  import { httpError } from '../http-errors.js';
29
31
  import type {
@@ -34,48 +36,41 @@ import type {
34
36
  RuntimeMessagePayload,
35
37
  SendMessageDeps,
36
38
  } from '../http-types.js';
39
+ import { resolveLocalIpcGuardianContext } from '../local-actor-identity.js';
40
+ import { type ServerWithRequestIP, verifyHttpActorTokenWithLocalFallback } from '../middleware/actor-token.js';
37
41
  import * as pendingInteractions from '../pending-interactions.js';
38
42
 
39
43
  const log = getLogger('conversation-routes');
40
44
 
41
45
  const SUGGESTION_CACHE_MAX = 100;
42
46
 
43
- function collectLivePendingConfirmationRequestIds(
47
+ function collectCanonicalGuardianRequestHintIds(
44
48
  conversationId: string,
45
49
  sourceChannel: string,
46
50
  session: import('../../daemon/session.js').Session,
47
51
  ): string[] {
48
- const pendingInteractionRequestIds = pendingInteractions
49
- .getByConversation(conversationId)
50
- .filter(
51
- (interaction) =>
52
- interaction.kind === 'confirmation'
53
- && interaction.session === session
54
- && session.hasPendingConfirmation(interaction.requestId),
55
- )
56
- .map((interaction) => interaction.requestId);
57
-
58
- // Query both by destination conversation (via deliveries table) and by
59
- // source conversation (direct field). For desktop/HTTP sessions these
60
- // often overlap, but the Set dedup below handles that.
61
- const pendingCanonicalRequestIds = [
52
+ const requests = [
62
53
  ...listPendingCanonicalGuardianRequestsByDestinationConversation(conversationId, sourceChannel)
63
- .filter((request) => request.kind === 'tool_approval')
64
- .map((request) => request.id),
54
+ .map((request) => ({ id: request.id, kind: request.kind })),
65
55
  ...listCanonicalGuardianRequests({
66
56
  status: 'pending',
67
57
  conversationId,
68
- kind: 'tool_approval',
69
- }).map((request) => request.id),
70
- ].filter((requestId) => session.hasPendingConfirmation(requestId));
71
-
72
- return Array.from(new Set([
73
- ...pendingInteractionRequestIds,
74
- ...pendingCanonicalRequestIds,
75
- ]));
58
+ }).map((request) => ({ id: request.id, kind: request.kind })),
59
+ ];
60
+
61
+ const deduped = new Map<string, string>();
62
+ for (const request of requests) {
63
+ if (!deduped.has(request.id)) {
64
+ deduped.set(request.id, request.kind ?? '');
65
+ }
66
+ }
67
+
68
+ return Array.from(deduped.entries())
69
+ .filter(([requestId, kind]) => kind !== 'tool_approval' || session.hasPendingConfirmation(requestId))
70
+ .map(([requestId]) => requestId);
76
71
  }
77
72
 
78
- async function tryConsumeInlineApprovalReply(params: {
73
+ async function tryConsumeCanonicalGuardianReply(params: {
79
74
  conversationId: string;
80
75
  sourceChannel: string;
81
76
  sourceInterface: string;
@@ -89,6 +84,8 @@ async function tryConsumeInlineApprovalReply(params: {
89
84
  session: import('../../daemon/session.js').Session;
90
85
  onEvent: (msg: ServerMessage) => void;
91
86
  approvalConversationGenerator?: ApprovalConversationGenerator;
87
+ /** Verified actor identity from actor-token middleware. */
88
+ verifiedActorExternalUserId?: string;
92
89
  }): Promise<{ consumed: boolean; messageId?: string }> {
93
90
  const {
94
91
  conversationId,
@@ -99,42 +96,57 @@ async function tryConsumeInlineApprovalReply(params: {
99
96
  session,
100
97
  onEvent,
101
98
  approvalConversationGenerator,
99
+ verifiedActorExternalUserId,
102
100
  } = params;
103
101
  const trimmedContent = content.trim();
104
102
 
105
- // Try inline approval interception whenever a pending confirmation exists.
106
- // We intentionally do not block on queue depth: after an auto-deny, users
107
- // often retry with "approve"/"yes" while the queue is still draining, and
108
- // requiring an empty queue can create a deny/retry cascade.
109
- if (
110
- !session.hasAnyPendingConfirmation()
111
- || trimmedContent.length === 0
112
- ) {
103
+ if (trimmedContent.length === 0) {
113
104
  return { consumed: false };
114
105
  }
115
106
 
116
- const pendingRequestIds = collectLivePendingConfirmationRequestIds(conversationId, sourceChannel, session);
117
- if (pendingRequestIds.length === 0) {
118
- return { consumed: false };
119
- }
107
+ const pendingRequestHintIds = collectCanonicalGuardianRequestHintIds(conversationId, sourceChannel, session);
108
+ const pendingRequestIds = pendingRequestHintIds.length > 0 ? pendingRequestHintIds : undefined;
120
109
 
121
110
  const routerResult = await routeGuardianReply({
122
111
  messageText: trimmedContent,
123
112
  channel: sourceChannel,
124
113
  actor: {
125
- externalUserId: undefined,
114
+ externalUserId: verifiedActorExternalUserId,
126
115
  channel: sourceChannel,
127
- isTrusted: true,
116
+ // When a verified identity is available, disable the trusted bypass so
117
+ // that the identity-match checks in applyCanonicalGuardianDecision
118
+ // actually run. Only fall back to isTrusted when no verified identity
119
+ // was resolved (defensive — shouldn't happen for vellum since
120
+ // verification runs upstream).
121
+ isTrusted: !verifiedActorExternalUserId,
128
122
  },
129
123
  conversationId,
130
124
  pendingRequestIds,
131
125
  approvalConversationGenerator,
126
+ emissionContext: {
127
+ source: 'inline_nl',
128
+ decisionText: trimmedContent,
129
+ },
132
130
  });
133
131
 
134
132
  if (!routerResult.consumed || routerResult.type === 'nl_keep_pending') {
135
133
  return { consumed: false };
136
134
  }
137
135
 
136
+ // Success-path emissions (approved/denied) are handled centrally
137
+ // by handleConfirmationResponse (called via the resolver chain).
138
+ // However, stale/failed paths never reach handleConfirmationResponse,
139
+ // so we emit resolved_stale here for those cases.
140
+ if (routerResult.requestId && !routerResult.decisionApplied) {
141
+ session.emitConfirmationStateChanged({
142
+ sessionId: conversationId,
143
+ requestId: routerResult.requestId,
144
+ state: 'resolved_stale',
145
+ source: 'inline_nl',
146
+ decisionText: trimmedContent,
147
+ });
148
+ }
149
+
138
150
  // Decision has been applied — transcript persistence is best-effort.
139
151
  // If DB writes fail, we still return consumed: true so the approval text
140
152
  // is not re-processed as a new user turn.
@@ -335,7 +347,7 @@ function makeHubPublisher(
335
347
  // via applyCanonicalGuardianDecision.
336
348
  const guardianContext = session.guardianContext;
337
349
  const sourceChannel = guardianContext?.sourceChannel ?? 'vellum';
338
- createCanonicalGuardianRequest({
350
+ const canonicalRequest = createCanonicalGuardianRequest({
339
351
  id: msg.requestId,
340
352
  kind: 'tool_approval',
341
353
  sourceType: resolveCanonicalRequestSourceType(sourceChannel),
@@ -349,6 +361,18 @@ function makeHubPublisher(
349
361
  requestCode: generateCanonicalRequestCode(),
350
362
  expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
351
363
  });
364
+
365
+ // For trusted-contact sessions, bridge to guardian.question so the
366
+ // guardian gets notified and can approve via callback/request-code.
367
+ if (guardianContext) {
368
+ bridgeConfirmationRequestToGuardian({
369
+ canonicalRequest,
370
+ guardianContext,
371
+ conversationId,
372
+ toolName: msg.toolName,
373
+ assistantId: session.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
374
+ });
375
+ }
352
376
  } else if (msg.type === 'secret_request') {
353
377
  pendingInteractions.register(msg.requestId, {
354
378
  session,
@@ -363,7 +387,7 @@ function makeHubPublisher(
363
387
  ? (msg as { sessionId: string }).sessionId
364
388
  : undefined;
365
389
  const resolvedSessionId = msgSessionId ?? conversationId;
366
- const event = buildAssistantEvent('self', msg, resolvedSessionId);
390
+ const event = buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg, resolvedSessionId);
367
391
  hubChain = (async () => {
368
392
  await hubChain;
369
393
  try {
@@ -383,6 +407,7 @@ export async function handleSendMessage(
383
407
  sendMessageDeps?: SendMessageDeps;
384
408
  approvalConversationGenerator?: ApprovalConversationGenerator;
385
409
  },
410
+ server: ServerWithRequestIP,
386
411
  ): Promise<Response> {
387
412
  const body = await req.json() as {
388
413
  conversationKey?: string;
@@ -440,31 +465,68 @@ export async function handleSendMessage(
440
465
 
441
466
  // ── Queue-if-busy path (preferred when sendMessageDeps is wired) ────
442
467
  if (deps.sendMessageDeps) {
468
+ // Vellum HTTP requests prefer actor-token identity. When absent (e.g. CLI
469
+ // bearer-auth only), fall back to local IPC identity resolution so
470
+ // bearer-authenticated local clients are not rejected.
471
+ const actorVerification = sourceChannel === 'vellum' ? verifyHttpActorTokenWithLocalFallback(req, server) : null;
472
+ if (actorVerification && !actorVerification.ok) {
473
+ return httpError(
474
+ actorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
475
+ actorVerification.message,
476
+ actorVerification.status,
477
+ );
478
+ }
479
+
443
480
  const smDeps = deps.sendMessageDeps;
444
481
  const session = await smDeps.getOrCreateSession(mapping.conversationId);
445
- // HTTP API is a trusted local ingress (same as IPC) — set guardian context
446
- // so that memory extraction is not silently disabled by unverified provenance.
447
- session.setGuardianContext({ trustClass: 'guardian', sourceChannel: sourceChannel ?? 'http' });
482
+ // Resolve actor identity from the verified actor token. The token's
483
+ // guardianPrincipalId is matched against the vellum guardian binding
484
+ // through the standard trust pipeline.
485
+ if (actorVerification?.ok) {
486
+ session.setGuardianContext(actorVerification.guardianContext);
487
+ } else {
488
+ // Non-vellum channels through the HTTP API are still local
489
+ // authenticated requests. Resolve guardian context via the local
490
+ // identity pathway (vellum binding lookup) to preserve guardian
491
+ // trust. Falls back to a minimal guardian context if no binding
492
+ // exists (pre-bootstrap).
493
+ session.setGuardianContext(
494
+ resolveLocalIpcGuardianContext(sourceChannel) ?? { trustClass: 'guardian', sourceChannel },
495
+ );
496
+ }
448
497
  const onEvent = makeHubPublisher(smDeps, mapping.conversationId, session);
498
+ // Route server-authoritative state signals (confirmation_state_changed,
499
+ // assistant_activity_state) to the SSE hub. Without this, these signals
500
+ // only travel through session.sendToClient, which is a no-op for
501
+ // socketless HTTP sessions.
502
+ session.setStateSignalListener(onEvent);
449
503
 
450
504
  const attachments = hasAttachments
451
505
  ? smDeps.resolveAttachments(attachmentIds)
452
506
  : [];
453
507
 
454
- // Try to consume the message as an inline approval/rejection reply.
508
+ // Resolve the verified actor's external user ID for inline approval
509
+ // routing. Uses the guardianExternalUserId from the verified context
510
+ // (actor-token or local-fallback) rather than hardcoding undefined.
511
+ const verifiedActorExternalUserId = actorVerification?.ok
512
+ ? actorVerification.guardianContext.guardianExternalUserId
513
+ : undefined;
514
+
515
+ // Try to consume the message as a canonical guardian approval/rejection reply.
455
516
  // On failure, degrade to the existing queue/auto-deny path rather than
456
517
  // surfacing a 500 — mirrors the IPC handler's catch-and-fallback.
457
518
  try {
458
- const inlineReplyResult = await tryConsumeInlineApprovalReply({
519
+ const inlineReplyResult = await tryConsumeCanonicalGuardianReply({
459
520
  conversationId: mapping.conversationId,
460
521
  sourceChannel,
461
522
  sourceInterface,
462
523
  content: content ?? '',
463
- attachments,
464
- session,
465
- onEvent,
466
- approvalConversationGenerator: deps.approvalConversationGenerator,
467
- });
524
+ attachments,
525
+ session,
526
+ onEvent,
527
+ approvalConversationGenerator: deps.approvalConversationGenerator,
528
+ verifiedActorExternalUserId,
529
+ });
468
530
  if (inlineReplyResult.consumed) {
469
531
  return Response.json(
470
532
  { accepted: true, ...(inlineReplyResult.messageId ? { messageId: inlineReplyResult.messageId } : {}) },
@@ -479,6 +541,18 @@ export async function handleSendMessage(
479
541
  // If a tool confirmation is pending, auto-deny it so the agent
480
542
  // can finish the current turn and process this queued message.
481
543
  if (session.hasAnyPendingConfirmation()) {
544
+ // Emit authoritative denial state for each pending request.
545
+ // The onStateSignal listener routes these to the SSE hub automatically.
546
+ for (const interaction of pendingInteractions.getByConversation(mapping.conversationId)) {
547
+ if (interaction.session === session && interaction.kind === 'confirmation') {
548
+ session.emitConfirmationStateChanged({
549
+ sessionId: mapping.conversationId,
550
+ requestId: interaction.requestId,
551
+ state: 'denied' as const,
552
+ source: 'auto_deny' as const,
553
+ });
554
+ }
555
+ }
482
556
  session.denyAllPendingConfirmations();
483
557
  pendingInteractions.removeBySession(session);
484
558
  }
@@ -533,12 +607,27 @@ export async function handleSendMessage(
533
607
  return httpError('SERVICE_UNAVAILABLE', 'Message processing not configured', 503);
534
608
  }
535
609
 
610
+ // Require actor token for vellum channel requests on the legacy path too,
611
+ // with local IPC fallback for bearer-authenticated CLI clients.
612
+ const legacyActorVerification = sourceChannel === 'vellum' ? verifyHttpActorTokenWithLocalFallback(req, server) : null;
613
+ if (legacyActorVerification && !legacyActorVerification.ok) {
614
+ return httpError(
615
+ legacyActorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
616
+ legacyActorVerification.message,
617
+ legacyActorVerification.status,
618
+ );
619
+ }
620
+
621
+ const guardianContext = legacyActorVerification?.ok
622
+ ? legacyActorVerification.guardianContext
623
+ : resolveLocalIpcGuardianContext(sourceChannel) ?? { trustClass: 'guardian' as const, sourceChannel };
624
+
536
625
  try {
537
626
  const result = await processor(
538
627
  mapping.conversationId,
539
628
  content ?? '',
540
629
  hasAttachments ? attachmentIds : undefined,
541
- { guardianContext: { trustClass: 'guardian', sourceChannel } },
630
+ { guardianContext },
542
631
  sourceChannel,
543
632
  sourceInterface,
544
633
  );