@vellumai/assistant 0.4.3 → 0.4.4

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