@vellumai/assistant 0.4.11 → 0.4.13

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 (111) hide show
  1. package/ARCHITECTURE.md +401 -385
  2. package/package.json +1 -1
  3. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  4. package/src/__tests__/registry.test.ts +235 -187
  5. package/src/__tests__/secure-keys.test.ts +27 -0
  6. package/src/__tests__/session-agent-loop.test.ts +521 -256
  7. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  8. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  9. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  10. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  11. package/src/__tests__/skills.test.ts +334 -276
  12. package/src/__tests__/slack-skill.test.ts +124 -0
  13. package/src/__tests__/starter-task-flow.test.ts +7 -17
  14. package/src/agent/loop.ts +10 -3
  15. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  16. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  17. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  18. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  19. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  20. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  21. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  22. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  23. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  24. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  25. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  26. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  27. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  28. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  29. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  30. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  31. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  32. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  33. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  34. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  35. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  36. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  37. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  38. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
  39. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  40. package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
  41. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  42. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  43. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
  44. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
  45. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  46. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  47. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
  48. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  49. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  50. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  51. package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
  52. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  53. package/src/config/bundled-skills/slack/SKILL.md +49 -0
  54. package/src/config/bundled-skills/slack/TOOLS.json +167 -0
  55. package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
  56. package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
  57. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
  58. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
  59. package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
  60. package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
  61. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
  62. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  63. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  64. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
  65. package/src/config/bundled-tool-registry.ts +292 -267
  66. package/src/config/schema.ts +1 -1
  67. package/src/daemon/handlers/skills.ts +334 -234
  68. package/src/daemon/ipc-contract/messages.ts +2 -0
  69. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  70. package/src/daemon/lifecycle.ts +358 -221
  71. package/src/daemon/response-tier.ts +2 -0
  72. package/src/daemon/server.ts +453 -193
  73. package/src/daemon/session-agent-loop-handlers.ts +43 -2
  74. package/src/daemon/session-agent-loop.ts +3 -0
  75. package/src/daemon/session-lifecycle.ts +3 -0
  76. package/src/daemon/session-process.ts +1 -0
  77. package/src/daemon/session-surfaces.ts +22 -20
  78. package/src/daemon/session-tool-setup.ts +1 -0
  79. package/src/daemon/session.ts +5 -2
  80. package/src/messaging/outreach-classifier.ts +12 -5
  81. package/src/messaging/provider-types.ts +5 -0
  82. package/src/messaging/provider.ts +1 -1
  83. package/src/messaging/providers/gmail/adapter.ts +11 -5
  84. package/src/messaging/providers/gmail/client.ts +2 -0
  85. package/src/messaging/providers/slack/adapter.ts +1 -0
  86. package/src/messaging/providers/slack/client.ts +8 -0
  87. package/src/messaging/providers/slack/types.ts +5 -0
  88. package/src/runtime/http-errors.ts +33 -20
  89. package/src/runtime/http-server.ts +706 -291
  90. package/src/runtime/http-types.ts +26 -16
  91. package/src/runtime/routes/secret-routes.ts +57 -2
  92. package/src/runtime/routes/surface-action-routes.ts +66 -0
  93. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  94. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  95. package/src/security/secure-keys.ts +17 -0
  96. package/src/skills/frontmatter.ts +9 -7
  97. package/src/tools/apps/executors.ts +2 -1
  98. package/src/tools/tool-manifest.ts +44 -42
  99. package/src/tools/types.ts +9 -0
  100. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  101. package/src/config/vellum-skills/catalog.json +0 -63
  102. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  103. package/src/skills/vellum-catalog-remote.ts +0 -166
  104. package/src/tools/skills/vellum-catalog.ts +0 -168
  105. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  106. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  107. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  108. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  109. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  110. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  111. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -5,44 +5,55 @@
5
5
  * configured port (default: 7821).
6
6
  */
7
7
 
8
- import { existsSync, readFileSync } from 'node:fs';
9
- import { homedir } from 'node:os';
10
- import { join, resolve } from 'node:path';
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join, resolve } from "node:path";
11
11
 
12
- import type { ServerWebSocket } from 'bun';
12
+ import type { ServerWebSocket } from "bun";
13
13
 
14
- import type { BrowserRelayWebSocketData } from '../browser-extension-relay/server.js';
15
- import { extensionRelayServer } from '../browser-extension-relay/server.js';
14
+ import type { BrowserRelayWebSocketData } from "../browser-extension-relay/server.js";
15
+ import { extensionRelayServer } from "../browser-extension-relay/server.js";
16
16
  import {
17
17
  startGuardianActionSweep,
18
18
  stopGuardianActionSweep,
19
- } from '../calls/guardian-action-sweep.js';
20
- import type { RelayWebSocketData } from '../calls/relay-server.js';
21
- import { activeRelayConnections,RelayConnection } from '../calls/relay-server.js';
19
+ } from "../calls/guardian-action-sweep.js";
20
+ import type { RelayWebSocketData } from "../calls/relay-server.js";
21
+ import {
22
+ activeRelayConnections,
23
+ RelayConnection,
24
+ } from "../calls/relay-server.js";
22
25
  import {
23
26
  handleConnectAction,
24
27
  handleStatusCallback,
25
28
  handleVoiceWebhook,
26
- } from '../calls/twilio-routes.js';
27
- import { parseChannelId } from '../channels/types.js';
29
+ } from "../calls/twilio-routes.js";
30
+ import { parseChannelId } from "../channels/types.js";
28
31
  import {
29
32
  getGatewayInternalBaseUrl,
30
33
  getRuntimeGatewayOriginSecret,
31
34
  hasUngatedHttpAuthDisabled,
32
35
  isHttpAuthDisabled,
33
- } from '../config/env.js';
34
- import type { ServerMessage } from '../daemon/ipc-contract.js';
35
- import { PairingStore } from '../daemon/pairing-store.js';
36
- import { type Confidence, getAttentionStateByConversationIds, recordConversationSeenSignal,type SignalType } from '../memory/conversation-attention-store.js';
37
- import * as conversationStore from '../memory/conversation-store.js';
38
- import * as externalConversationStore from '../memory/external-conversation-store.js';
39
- import { consumeCallback, consumeCallbackError } from '../security/oauth-callback-registry.js';
40
- import { getLogger } from '../util/logger.js';
41
- import { buildAssistantEvent } from './assistant-event.js';
42
- import { assistantEventHub } from './assistant-event-hub.js';
43
- import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
44
- import { sweepFailedEvents } from './channel-retry-sweep.js';
45
- import { httpError } from './http-errors.js';
36
+ } from "../config/env.js";
37
+ import type { ServerMessage } from "../daemon/ipc-contract.js";
38
+ import { PairingStore } from "../daemon/pairing-store.js";
39
+ import {
40
+ type Confidence,
41
+ getAttentionStateByConversationIds,
42
+ recordConversationSeenSignal,
43
+ type SignalType,
44
+ } from "../memory/conversation-attention-store.js";
45
+ import * as conversationStore from "../memory/conversation-store.js";
46
+ import * as externalConversationStore from "../memory/external-conversation-store.js";
47
+ import {
48
+ consumeCallback,
49
+ consumeCallbackError,
50
+ } from "../security/oauth-callback-registry.js";
51
+ import { getLogger } from "../util/logger.js";
52
+ import { buildAssistantEvent } from "./assistant-event.js";
53
+ import { assistantEventHub } from "./assistant-event-hub.js";
54
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
55
+ import { sweepFailedEvents } from "./channel-retry-sweep.js";
56
+ import { httpError } from "./http-errors.js";
46
57
  // Middleware
47
58
  import {
48
59
  extractBearerToken,
@@ -50,16 +61,16 @@ import {
50
61
  isPrivateNetworkOrigin,
51
62
  isPrivateNetworkPeer,
52
63
  verifyBearerToken,
53
- } from './middleware/auth.js';
54
- import { withErrorHandling } from './middleware/error-handler.js';
64
+ } from "./middleware/auth.js";
65
+ import { withErrorHandling } from "./middleware/error-handler.js";
55
66
  import {
56
67
  apiRateLimiter,
57
68
  extractClientIp,
58
69
  ipRateLimiter,
59
70
  rateLimitHeaders,
60
71
  rateLimitResponse,
61
- } from './middleware/rate-limiter.js';
62
- import { withRequestLogging } from './middleware/request-logger.js';
72
+ } from "./middleware/rate-limiter.js";
73
+ import { withRequestLogging } from "./middleware/request-logger.js";
63
74
  import {
64
75
  cloneRequestWithBody,
65
76
  GATEWAY_ONLY_BLOCKED_SUBPATHS,
@@ -67,42 +78,46 @@ import {
67
78
  TWILIO_GATEWAY_WEBHOOK_RE,
68
79
  TWILIO_WEBHOOK_RE,
69
80
  validateTwilioWebhook,
70
- } from './middleware/twilio-validation.js';
81
+ } from "./middleware/twilio-validation.js";
71
82
  import {
72
83
  handleDeleteSharedApp,
73
84
  handleDownloadSharedApp,
74
85
  handleGetSharedAppMetadata,
75
86
  handleServePage,
76
87
  handleShareApp,
77
- } from './routes/app-routes.js';
88
+ } from "./routes/app-routes.js";
78
89
  import {
79
90
  handleConfirm,
80
91
  handleListPendingInteractions,
81
92
  handleSecret,
82
93
  handleTrustRule,
83
- } from './routes/approval-routes.js';
94
+ } from "./routes/approval-routes.js";
84
95
  import {
85
96
  handleDeleteAttachment,
86
97
  handleGetAttachment,
87
98
  handleGetAttachmentContent,
88
99
  handleUploadAttachment,
89
- } from './routes/attachment-routes.js';
90
- import { handleGetBrainGraph, handleServeBrainGraphUI, handleServeHomeBaseUI } from './routes/brain-graph-routes.js';
100
+ } from "./routes/attachment-routes.js";
101
+ import {
102
+ handleGetBrainGraph,
103
+ handleServeBrainGraphUI,
104
+ handleServeHomeBaseUI,
105
+ } from "./routes/brain-graph-routes.js";
91
106
  import {
92
107
  handleAnswerCall,
93
108
  handleCancelCall,
94
109
  handleGetCallStatus,
95
110
  handleInstructionCall,
96
111
  handleStartCall,
97
- } from './routes/call-routes.js';
112
+ } from "./routes/call-routes.js";
98
113
  import {
99
114
  startCanonicalGuardianExpirySweep,
100
115
  stopCanonicalGuardianExpirySweep,
101
- } from './routes/canonical-guardian-expiry-sweep.js';
116
+ } from "./routes/canonical-guardian-expiry-sweep.js";
102
117
  import {
103
118
  handleGetChannelReadiness,
104
119
  handleRefreshChannelReadiness,
105
- } from './routes/channel-readiness-routes.js';
120
+ } from "./routes/channel-readiness-routes.js";
106
121
  import {
107
122
  handleChannelDeliveryAck,
108
123
  handleChannelInbound,
@@ -111,29 +126,29 @@ import {
111
126
  handleReplayDeadLetters,
112
127
  startGuardianExpirySweep,
113
128
  stopGuardianExpirySweep,
114
- } from './routes/channel-routes.js';
129
+ } from "./routes/channel-routes.js";
115
130
  import {
116
131
  handleGetContact,
117
132
  handleListContacts,
118
133
  handleMergeContacts,
119
- } from './routes/contact-routes.js';
120
- import { handleListConversationAttention } from './routes/conversation-attention-routes.js';
134
+ } from "./routes/contact-routes.js";
135
+ import { handleListConversationAttention } from "./routes/conversation-attention-routes.js";
121
136
  // Route handlers — grouped by domain
122
137
  import {
123
138
  handleGetSuggestion,
124
139
  handleListMessages,
125
140
  handleSearchConversations,
126
141
  handleSendMessage,
127
- } from './routes/conversation-routes.js';
128
- import { handleDebug } from './routes/debug-routes.js';
129
- import { handleSubscribeAssistantEvents } from './routes/events-routes.js';
142
+ } from "./routes/conversation-routes.js";
143
+ import { handleDebug } from "./routes/debug-routes.js";
144
+ import { handleSubscribeAssistantEvents } from "./routes/events-routes.js";
130
145
  import {
131
146
  handleGuardianActionDecision,
132
147
  handleGuardianActionsPending,
133
- } from './routes/guardian-action-routes.js';
134
- import { handleGuardianBootstrap } from './routes/guardian-bootstrap-routes.js';
135
- import { handleGuardianRefresh } from './routes/guardian-refresh-routes.js';
136
- import { handleGetIdentity,handleHealth } from './routes/identity-routes.js';
148
+ } from "./routes/guardian-action-routes.js";
149
+ import { handleGuardianBootstrap } from "./routes/guardian-bootstrap-routes.js";
150
+ import { handleGuardianRefresh } from "./routes/guardian-refresh-routes.js";
151
+ import { handleGetIdentity, handleHealth } from "./routes/identity-routes.js";
137
152
  import {
138
153
  handleBlockMember,
139
154
  handleCreateInvite,
@@ -143,7 +158,7 @@ import {
143
158
  handleRevokeInvite,
144
159
  handleRevokeMember,
145
160
  handleUpsertMember,
146
- } from './routes/ingress-routes.js';
161
+ } from "./routes/ingress-routes.js";
147
162
  import {
148
163
  handleCancelOutbound,
149
164
  handleClearSlackChannelConfig,
@@ -158,15 +173,22 @@ import {
158
173
  handleSetTelegramConfig,
159
174
  handleSetupTelegram,
160
175
  handleStartOutbound,
161
- } from './routes/integration-routes.js';
162
- import type { PairingHandlerContext } from './routes/pairing-routes.js';
176
+ } from "./routes/integration-routes.js";
177
+ import type { PairingHandlerContext } from "./routes/pairing-routes.js";
163
178
  // Extracted route handlers
164
179
  import {
165
180
  handlePairingRegister,
166
181
  handlePairingRequest,
167
182
  handlePairingStatus,
168
- } from './routes/pairing-routes.js';
169
- import { handleAddSecret } from './routes/secret-routes.js';
183
+ } from "./routes/pairing-routes.js";
184
+ import { handleAddSecret, handleDeleteSecret } from "./routes/secret-routes.js";
185
+ import { handleSurfaceAction } from "./routes/surface-action-routes.js";
186
+ import {
187
+ handleAddTrustRuleManage,
188
+ handleListTrustRules,
189
+ handleRemoveTrustRuleManage,
190
+ handleUpdateTrustRuleManage,
191
+ } from "./routes/trust-rules-routes.js";
170
192
  import {
171
193
  handleAssignTwilioNumber,
172
194
  handleClearTwilioCredentials,
@@ -181,10 +203,10 @@ import {
181
203
  handleSmsSendTest,
182
204
  handleSubmitTollfreeVerification,
183
205
  handleUpdateTollfreeVerification,
184
- } from './routes/twilio-routes.js';
206
+ } from "./routes/twilio-routes.js";
185
207
 
186
208
  // Re-export for consumers
187
- export { isPrivateAddress } from './middleware/auth.js';
209
+ export { isPrivateAddress } from "./middleware/auth.js";
188
210
 
189
211
  // Re-export shared types so existing consumers don't need to update imports
190
212
  export type {
@@ -198,7 +220,7 @@ export type {
198
220
  RuntimeHttpServerOptions,
199
221
  RuntimeMessageSessionOptions,
200
222
  SendMessageDeps,
201
- } from './http-types.js';
223
+ } from "./http-types.js";
202
224
 
203
225
  import type {
204
226
  ApprovalConversationGenerator,
@@ -209,12 +231,12 @@ import type {
209
231
  NonBlockingMessageProcessor,
210
232
  RuntimeHttpServerOptions,
211
233
  SendMessageDeps,
212
- } from './http-types.js';
234
+ } from "./http-types.js";
213
235
 
214
- const log = getLogger('runtime-http');
236
+ const log = getLogger("runtime-http");
215
237
 
216
238
  const DEFAULT_PORT = 7821;
217
- const DEFAULT_HOSTNAME = '127.0.0.1';
239
+ const DEFAULT_HOSTNAME = "127.0.0.1";
218
240
 
219
241
  /** Global hard cap on request body size (50 MB). */
220
242
  const MAX_REQUEST_BODY_BYTES = 50 * 1024 * 1024;
@@ -238,6 +260,9 @@ export class RuntimeHttpServer {
238
260
  private pairingStore = new PairingStore();
239
261
  private pairingBroadcast?: (msg: ServerMessage) => void;
240
262
  private sendMessageDeps?: SendMessageDeps;
263
+ private findSession?: (
264
+ sessionId: string,
265
+ ) => import("../daemon/session.js").Session | undefined;
241
266
 
242
267
  constructor(options: RuntimeHttpServerOptions = {}) {
243
268
  this.port = options.port ?? DEFAULT_PORT;
@@ -248,9 +273,11 @@ export class RuntimeHttpServer {
248
273
  this.approvalCopyGenerator = options.approvalCopyGenerator;
249
274
  this.approvalConversationGenerator = options.approvalConversationGenerator;
250
275
  this.guardianActionCopyGenerator = options.guardianActionCopyGenerator;
251
- this.guardianFollowUpConversationGenerator = options.guardianFollowUpConversationGenerator;
276
+ this.guardianFollowUpConversationGenerator =
277
+ options.guardianFollowUpConversationGenerator;
252
278
  this.interfacesDir = options.interfacesDir ?? null;
253
279
  this.sendMessageDeps = options.sendMessageDeps;
280
+ this.findSession = options.findSession;
254
281
  }
255
282
 
256
283
  /** The port the server is actually listening on (resolved after start). */
@@ -272,8 +299,8 @@ export class RuntimeHttpServer {
272
299
  private readFeatureFlagToken(): string | undefined {
273
300
  try {
274
301
  const baseDir = process.env.BASE_DATA_DIR?.trim() || homedir();
275
- const tokenPath = join(baseDir, '.vellum', 'feature-flag-token');
276
- const token = readFileSync(tokenPath, 'utf-8').trim();
302
+ const tokenPath = join(baseDir, ".vellum", "feature-flag-token");
303
+ const token = readFileSync(tokenPath, "utf-8").trim();
277
304
  return token || undefined;
278
305
  } catch {
279
306
  return undefined;
@@ -292,7 +319,9 @@ export class RuntimeHttpServer {
292
319
  ipcBroadcast(msg);
293
320
  // Also publish to the event hub so HTTP/SSE clients (e.g. macOS
294
321
  // app with localHttpEnabled) receive pairing approval requests.
295
- void assistantEventHub.publish(buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg));
322
+ void assistantEventHub.publish(
323
+ buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg),
324
+ );
296
325
  }
297
326
  : undefined,
298
327
  };
@@ -309,22 +338,33 @@ export class RuntimeHttpServer {
309
338
  websocket: {
310
339
  open(ws) {
311
340
  const data = ws.data as AllWebSocketData;
312
- if ('wsType' in data && data.wsType === 'browser-relay') {
313
- extensionRelayServer.handleOpen(ws as ServerWebSocket<BrowserRelayWebSocketData>);
341
+ if ("wsType" in data && data.wsType === "browser-relay") {
342
+ extensionRelayServer.handleOpen(
343
+ ws as ServerWebSocket<BrowserRelayWebSocketData>,
344
+ );
314
345
  return;
315
346
  }
316
347
  const callSessionId = (data as RelayWebSocketData).callSessionId;
317
- log.info({ callSessionId }, 'ConversationRelay WebSocket opened');
348
+ log.info({ callSessionId }, "ConversationRelay WebSocket opened");
318
349
  if (callSessionId) {
319
- const connection = new RelayConnection(ws as ServerWebSocket<RelayWebSocketData>, callSessionId);
350
+ const connection = new RelayConnection(
351
+ ws as ServerWebSocket<RelayWebSocketData>,
352
+ callSessionId,
353
+ );
320
354
  activeRelayConnections.set(callSessionId, connection);
321
355
  }
322
356
  },
323
357
  message(ws, message) {
324
358
  const data = ws.data as AllWebSocketData;
325
- const raw = typeof message === 'string' ? message : new TextDecoder().decode(message);
326
- if ('wsType' in data && data.wsType === 'browser-relay') {
327
- extensionRelayServer.handleMessage(ws as ServerWebSocket<BrowserRelayWebSocketData>, raw);
359
+ const raw =
360
+ typeof message === "string"
361
+ ? message
362
+ : new TextDecoder().decode(message);
363
+ if ("wsType" in data && data.wsType === "browser-relay") {
364
+ extensionRelayServer.handleMessage(
365
+ ws as ServerWebSocket<BrowserRelayWebSocketData>,
366
+ raw,
367
+ );
328
368
  return;
329
369
  }
330
370
  const callSessionId = (data as RelayWebSocketData).callSessionId;
@@ -335,12 +375,19 @@ export class RuntimeHttpServer {
335
375
  },
336
376
  close(ws, code, reason) {
337
377
  const data = ws.data as AllWebSocketData;
338
- if ('wsType' in data && data.wsType === 'browser-relay') {
339
- extensionRelayServer.handleClose(ws as ServerWebSocket<BrowserRelayWebSocketData>, code, reason?.toString());
378
+ if ("wsType" in data && data.wsType === "browser-relay") {
379
+ extensionRelayServer.handleClose(
380
+ ws as ServerWebSocket<BrowserRelayWebSocketData>,
381
+ code,
382
+ reason?.toString(),
383
+ );
340
384
  return;
341
385
  }
342
386
  const callSessionId = (data as RelayWebSocketData).callSessionId;
343
- log.info({ callSessionId, code, reason: reason?.toString() }, 'ConversationRelay WebSocket closed');
387
+ log.info(
388
+ { callSessionId, code, reason: reason?.toString() },
389
+ "ConversationRelay WebSocket closed",
390
+ );
344
391
  if (callSessionId) {
345
392
  const connection = activeRelayConnections.get(callSessionId);
346
393
  connection?.handleTransportClosed(code, reason?.toString());
@@ -357,33 +404,58 @@ export class RuntimeHttpServer {
357
404
  this.retrySweepTimer = setInterval(() => {
358
405
  if (this.sweepInProgress) return;
359
406
  this.sweepInProgress = true;
360
- sweepFailedEvents(pm, bt).finally(() => { this.sweepInProgress = false; });
407
+ sweepFailedEvents(pm, bt).finally(() => {
408
+ this.sweepInProgress = false;
409
+ });
361
410
  }, 30_000);
362
411
  }
363
412
 
364
- startGuardianExpirySweep(getGatewayInternalBaseUrl(), this.bearerToken, this.approvalCopyGenerator);
365
- log.info('Guardian approval expiry sweep started');
413
+ startGuardianExpirySweep(
414
+ getGatewayInternalBaseUrl(),
415
+ this.bearerToken,
416
+ this.approvalCopyGenerator,
417
+ );
418
+ log.info("Guardian approval expiry sweep started");
366
419
 
367
- startGuardianActionSweep(getGatewayInternalBaseUrl(), this.bearerToken, this.guardianActionCopyGenerator);
368
- log.info('Guardian action expiry sweep started');
420
+ startGuardianActionSweep(
421
+ getGatewayInternalBaseUrl(),
422
+ this.bearerToken,
423
+ this.guardianActionCopyGenerator,
424
+ );
425
+ log.info("Guardian action expiry sweep started");
369
426
 
370
427
  startCanonicalGuardianExpirySweep();
371
- log.info('Canonical guardian request expiry sweep started');
428
+ log.info("Canonical guardian request expiry sweep started");
372
429
 
373
- log.info('Running in gateway-only ingress mode. Direct webhook routes disabled.');
430
+ log.info(
431
+ "Running in gateway-only ingress mode. Direct webhook routes disabled.",
432
+ );
374
433
  if (!isLoopbackHost(this.hostname)) {
375
- log.warn('RUNTIME_HTTP_HOST is not bound to loopback. This may expose the runtime to direct public access.');
434
+ log.warn(
435
+ "RUNTIME_HTTP_HOST is not bound to loopback. This may expose the runtime to direct public access.",
436
+ );
376
437
  }
377
438
 
378
439
  this.pairingStore.start();
379
440
 
380
441
  if (hasUngatedHttpAuthDisabled()) {
381
- log.warn('DISABLE_HTTP_AUTH is set but VELLUM_UNSAFE_AUTH_BYPASS=1 is not — auth bypass is IGNORED and HTTP authentication remains enabled. Set VELLUM_UNSAFE_AUTH_BYPASS=1 to confirm the bypass.');
442
+ log.warn(
443
+ "DISABLE_HTTP_AUTH is set but VELLUM_UNSAFE_AUTH_BYPASS=1 is not — auth bypass is IGNORED and HTTP authentication remains enabled. Set VELLUM_UNSAFE_AUTH_BYPASS=1 to confirm the bypass.",
444
+ );
382
445
  } else if (isHttpAuthDisabled()) {
383
- log.warn('DISABLE_HTTP_AUTH is set — HTTP API authentication is DISABLED. All API endpoints are accessible without a bearer token. Do not use in production.');
446
+ log.warn(
447
+ "DISABLE_HTTP_AUTH is set — HTTP API authentication is DISABLED. All API endpoints are accessible without a bearer token. Do not use in production.",
448
+ );
384
449
  }
385
450
 
386
- log.info({ port: this.actualPort, hostname: this.hostname, auth: !!this.bearerToken }, 'Runtime HTTP server listening');
451
+ log.info(
452
+ {
453
+ port: this.actualPort,
454
+ hostname: this.hostname,
455
+ auth: !!this.bearerToken,
456
+ },
457
+ "Runtime HTTP server listening",
458
+ );
387
459
  }
388
460
 
389
461
  async stop(): Promise<void> {
@@ -398,36 +470,48 @@ export class RuntimeHttpServer {
398
470
  if (this.server) {
399
471
  this.server.stop(true);
400
472
  this.server = null;
401
- log.info('Runtime HTTP server stopped');
473
+ log.info("Runtime HTTP server stopped");
402
474
  }
403
475
  }
404
476
 
405
- private async handleRequest(req: Request, server: ReturnType<typeof Bun.serve>): Promise<Response> {
477
+ private async handleRequest(
478
+ req: Request,
479
+ server: ReturnType<typeof Bun.serve>,
480
+ ): Promise<Response> {
406
481
  server.timeout(req, 1800);
407
482
  // Skip request logging for health-check probes to reduce log noise.
408
483
  const url = new URL(req.url);
409
- if (url.pathname === '/healthz' && req.method === 'GET') {
484
+ if (url.pathname === "/healthz" && req.method === "GET") {
410
485
  return this.routeRequest(req, server);
411
486
  }
412
487
  return withRequestLogging(req, () => this.routeRequest(req, server));
413
488
  }
414
489
 
415
- private async routeRequest(req: Request, server: ReturnType<typeof Bun.serve>): Promise<Response> {
490
+ private async routeRequest(
491
+ req: Request,
492
+ server: ReturnType<typeof Bun.serve>,
493
+ ): Promise<Response> {
416
494
  const url = new URL(req.url);
417
495
  const path = url.pathname;
418
496
 
419
- if (path === '/healthz' && req.method === 'GET') {
497
+ if (path === "/healthz" && req.method === "GET") {
420
498
  return handleHealth();
421
499
  }
422
500
 
423
501
  // WebSocket upgrade for the Chrome extension browser relay.
424
- if (path === '/v1/browser-relay' && req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
502
+ if (
503
+ path === "/v1/browser-relay" &&
504
+ req.headers.get("upgrade")?.toLowerCase() === "websocket"
505
+ ) {
425
506
  return this.handleBrowserRelayUpgrade(req, server);
426
507
  }
427
508
 
428
509
  // WebSocket upgrade for ConversationRelay — before auth check because
429
510
  // Twilio WebSocket connections don't use bearer tokens.
430
- if (path.startsWith('/v1/calls/relay') && req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
511
+ if (
512
+ path.startsWith("/v1/calls/relay") &&
513
+ req.headers.get("upgrade")?.toLowerCase() === "websocket"
514
+ ) {
431
515
  return this.handleRelayUpgrade(req, server);
432
516
  }
433
517
 
@@ -437,10 +521,10 @@ export class RuntimeHttpServer {
437
521
  if (twilioResponse) return twilioResponse;
438
522
 
439
523
  // Pairing endpoints (unauthenticated, secret-gated)
440
- if (path === '/v1/pairing/request' && req.method === 'POST') {
524
+ if (path === "/v1/pairing/request" && req.method === "POST") {
441
525
  return await handlePairingRequest(req, this.pairingContext);
442
526
  }
443
- if (path === '/v1/pairing/status' && req.method === 'GET') {
527
+ if (path === "/v1/pairing/status" && req.method === "GET") {
444
528
  return handlePairingStatus(url, this.pairingContext);
445
529
  }
446
530
 
@@ -448,7 +532,7 @@ export class RuntimeHttpServer {
448
532
  if (!isHttpAuthDisabled() && this.bearerToken) {
449
533
  const token = extractBearerToken(req);
450
534
  if (!token || !verifyBearerToken(token, this.bearerToken)) {
451
- return httpError('UNAUTHORIZED', 'Unauthorized', 401);
535
+ return httpError("UNAUTHORIZED", "Unauthorized", 401);
452
536
  }
453
537
  }
454
538
 
@@ -457,7 +541,7 @@ export class RuntimeHttpServer {
457
541
  // abuse surface. We key on IP rather than bearer token because the gateway
458
542
  // uses a single shared token for all proxied requests, which would collapse
459
543
  // all users into one bucket.
460
- if (path.startsWith('/v1/')) {
544
+ if (path.startsWith("/v1/")) {
461
545
  const clientIp = extractClientIp(req, server);
462
546
  const token = extractBearerToken(req);
463
547
  const result = token
@@ -467,7 +551,12 @@ export class RuntimeHttpServer {
467
551
  return rateLimitResponse(result);
468
552
  }
469
553
  // Attach rate limit headers to the eventual response
470
- const originalResponse = await this.handleAuthenticatedRequest(req, url, path, server);
554
+ const originalResponse = await this.handleAuthenticatedRequest(
555
+ req,
556
+ url,
557
+ path,
558
+ server,
559
+ );
471
560
  const headers = new Headers(originalResponse.headers);
472
561
  for (const [k, v] of Object.entries(rateLimitHeaders(result))) {
473
562
  headers.set(k, v);
@@ -485,61 +574,98 @@ export class RuntimeHttpServer {
485
574
  /**
486
575
  * Handle requests that have already passed auth and rate limiting.
487
576
  */
488
- private async handleAuthenticatedRequest(req: Request, url: URL, path: string, server: ReturnType<typeof Bun.serve>): Promise<Response> {
577
+ private async handleAuthenticatedRequest(
578
+ req: Request,
579
+ url: URL,
580
+ path: string,
581
+ server: ReturnType<typeof Bun.serve>,
582
+ ): Promise<Response> {
489
583
  // Pairing registration (bearer-authenticated)
490
- if (path === '/v1/pairing/register' && req.method === 'POST') {
584
+ if (path === "/v1/pairing/register" && req.method === "POST") {
491
585
  return await handlePairingRegister(req, this.pairingContext);
492
586
  }
493
587
 
494
588
  // Serve shareable app pages
495
589
  const pagesMatch = path.match(/^\/pages\/([^/]+)$/);
496
- if (pagesMatch && req.method === 'GET') {
590
+ if (pagesMatch && req.method === "GET") {
497
591
  try {
498
592
  return handleServePage(pagesMatch[1]);
499
593
  } catch (err) {
500
- log.error({ err, appId: pagesMatch[1] }, 'Runtime HTTP handler error serving page');
501
- return httpError('INTERNAL_ERROR', 'Internal server error', 500);
594
+ log.error(
595
+ { err, appId: pagesMatch[1] },
596
+ "Runtime HTTP handler error serving page",
597
+ );
598
+ return httpError("INTERNAL_ERROR", "Internal server error", 500);
502
599
  }
503
600
  }
504
601
 
505
602
  // Cloud sharing endpoints
506
- if (path === '/v1/apps/share' && req.method === 'POST') {
507
- try { return await handleShareApp(req); } catch (err) {
508
- log.error({ err }, 'Runtime HTTP handler error sharing app');
509
- return httpError('INTERNAL_ERROR', 'Internal server error', 500);
603
+ if (path === "/v1/apps/share" && req.method === "POST") {
604
+ try {
605
+ return await handleShareApp(req);
606
+ } catch (err) {
607
+ log.error({ err }, "Runtime HTTP handler error sharing app");
608
+ return httpError("INTERNAL_ERROR", "Internal server error", 500);
510
609
  }
511
610
  }
512
611
 
513
612
  const sharedTokenMatch = path.match(/^\/v1\/apps\/shared\/([^/]+)$/);
514
613
  if (sharedTokenMatch) {
515
614
  const shareToken = sharedTokenMatch[1];
516
- if (req.method === 'GET') {
517
- try { return handleDownloadSharedApp(shareToken); } catch (err) {
518
- log.error({ err, shareToken }, 'Runtime HTTP handler error downloading shared app');
519
- return httpError('INTERNAL_ERROR', 'Internal server error', 500);
615
+ if (req.method === "GET") {
616
+ try {
617
+ return handleDownloadSharedApp(shareToken);
618
+ } catch (err) {
619
+ log.error(
620
+ { err, shareToken },
621
+ "Runtime HTTP handler error downloading shared app",
622
+ );
623
+ return httpError("INTERNAL_ERROR", "Internal server error", 500);
520
624
  }
521
625
  }
522
- if (req.method === 'DELETE') {
523
- try { return handleDeleteSharedApp(shareToken); } catch (err) {
524
- log.error({ err, shareToken }, 'Runtime HTTP handler error deleting shared app');
525
- return httpError('INTERNAL_ERROR', 'Internal server error', 500);
626
+ if (req.method === "DELETE") {
627
+ try {
628
+ return handleDeleteSharedApp(shareToken);
629
+ } catch (err) {
630
+ log.error(
631
+ { err, shareToken },
632
+ "Runtime HTTP handler error deleting shared app",
633
+ );
634
+ return httpError("INTERNAL_ERROR", "Internal server error", 500);
526
635
  }
527
636
  }
528
637
  }
529
638
 
530
- const sharedMetadataMatch = path.match(/^\/v1\/apps\/shared\/([^/]+)\/metadata$/);
531
- if (sharedMetadataMatch && req.method === 'GET') {
532
- try { return handleGetSharedAppMetadata(sharedMetadataMatch[1]); } catch (err) {
533
- log.error({ err, shareToken: sharedMetadataMatch[1] }, 'Runtime HTTP handler error getting shared app metadata');
534
- return httpError('INTERNAL_ERROR', 'Internal server error', 500);
639
+ const sharedMetadataMatch = path.match(
640
+ /^\/v1\/apps\/shared\/([^/]+)\/metadata$/,
641
+ );
642
+ if (sharedMetadataMatch && req.method === "GET") {
643
+ try {
644
+ return handleGetSharedAppMetadata(sharedMetadataMatch[1]);
645
+ } catch (err) {
646
+ log.error(
647
+ { err, shareToken: sharedMetadataMatch[1] },
648
+ "Runtime HTTP handler error getting shared app metadata",
649
+ );
650
+ return httpError("INTERNAL_ERROR", "Internal server error", 500);
535
651
  }
536
652
  }
537
653
 
538
654
  // Secret management endpoint
539
- if (path === '/v1/secrets' && req.method === 'POST') {
540
- try { return await handleAddSecret(req); } catch (err) {
541
- log.error({ err }, 'Runtime HTTP handler error adding secret');
542
- return httpError('INTERNAL_ERROR', 'Internal server error', 500);
655
+ if (path === "/v1/secrets" && req.method === "POST") {
656
+ try {
657
+ return await handleAddSecret(req);
658
+ } catch (err) {
659
+ log.error({ err }, "Runtime HTTP handler error adding secret");
660
+ return httpError("INTERNAL_ERROR", "Internal server error", 500);
661
+ }
662
+ }
663
+ if (path === "/v1/secrets" && req.method === "DELETE") {
664
+ try {
665
+ return await handleDeleteSecret(req);
666
+ } catch (err) {
667
+ log.error({ err }, "Runtime HTTP handler error deleting secret");
668
+ return httpError("INTERNAL_ERROR", "Internal server error", 500);
543
669
  }
544
670
  }
545
671
 
@@ -549,65 +675,94 @@ export class RuntimeHttpServer {
549
675
  return this.dispatchEndpoint(routeMatch[1], req, url, server);
550
676
  }
551
677
 
552
- return httpError('NOT_FOUND', 'Not found', 404);
678
+ return httpError("NOT_FOUND", "Not found", 404);
553
679
  }
554
680
 
555
- private handleBrowserRelayUpgrade(req: Request, server: ReturnType<typeof Bun.serve>): Response {
556
- if (!isLoopbackHost(new URL(req.url).hostname) && !isPrivateNetworkPeer(server, req)) {
557
- return httpError('FORBIDDEN', 'Browser relay only accepts connections from localhost', 403);
681
+ private handleBrowserRelayUpgrade(
682
+ req: Request,
683
+ server: ReturnType<typeof Bun.serve>,
684
+ ): Response {
685
+ if (
686
+ !isLoopbackHost(new URL(req.url).hostname) &&
687
+ !isPrivateNetworkPeer(server, req)
688
+ ) {
689
+ return httpError(
690
+ "FORBIDDEN",
691
+ "Browser relay only accepts connections from localhost",
692
+ 403,
693
+ );
558
694
  }
559
695
 
560
696
  if (!isHttpAuthDisabled() && this.bearerToken) {
561
697
  const wsUrl = new URL(req.url);
562
- const token = wsUrl.searchParams.get('token');
698
+ const token = wsUrl.searchParams.get("token");
563
699
  if (!token || !verifyBearerToken(token, this.bearerToken)) {
564
- return httpError('UNAUTHORIZED', 'Unauthorized', 401);
700
+ return httpError("UNAUTHORIZED", "Unauthorized", 401);
565
701
  }
566
702
  }
567
703
 
568
704
  const connectionId = crypto.randomUUID();
569
705
  const upgraded = server.upgrade(req, {
570
- data: { wsType: 'browser-relay', connectionId } satisfies BrowserRelayWebSocketData,
706
+ data: {
707
+ wsType: "browser-relay",
708
+ connectionId,
709
+ } satisfies BrowserRelayWebSocketData,
571
710
  });
572
711
  if (!upgraded) {
573
- return new Response('WebSocket upgrade failed', { status: 500 });
712
+ return new Response("WebSocket upgrade failed", { status: 500 });
574
713
  }
575
714
  // Bun's WebSocket upgrade consumes the request — no Response is sent.
576
715
  return undefined!;
577
716
  }
578
717
 
579
- private handleRelayUpgrade(req: Request, server: ReturnType<typeof Bun.serve>): Response {
718
+ private handleRelayUpgrade(
719
+ req: Request,
720
+ server: ReturnType<typeof Bun.serve>,
721
+ ): Response {
580
722
  if (!isPrivateNetworkPeer(server, req) || !isPrivateNetworkOrigin(req)) {
581
- return httpError('FORBIDDEN', 'Direct relay access disabled — only private network peers allowed', 403);
723
+ return httpError(
724
+ "FORBIDDEN",
725
+ "Direct relay access disabled — only private network peers allowed",
726
+ 403,
727
+ );
582
728
  }
583
729
 
584
730
  const wsUrl = new URL(req.url);
585
- const callSessionId = wsUrl.searchParams.get('callSessionId');
731
+ const callSessionId = wsUrl.searchParams.get("callSessionId");
586
732
  if (!callSessionId) {
587
- return new Response('Missing callSessionId', { status: 400 });
733
+ return new Response("Missing callSessionId", { status: 400 });
588
734
  }
589
735
  const upgraded = server.upgrade(req, { data: { callSessionId } });
590
736
  if (!upgraded) {
591
- return new Response('WebSocket upgrade failed', { status: 500 });
737
+ return new Response("WebSocket upgrade failed", { status: 500 });
592
738
  }
593
739
  // Bun's WebSocket upgrade consumes the request — no Response is sent.
594
740
  return undefined!;
595
741
  }
596
742
 
597
- private async handleTwilioWebhook(req: Request, path: string): Promise<Response | null> {
743
+ private async handleTwilioWebhook(
744
+ req: Request,
745
+ path: string,
746
+ ): Promise<Response | null> {
598
747
  const twilioMatch = path.match(TWILIO_WEBHOOK_RE);
599
- const gatewayTwilioMatch = !twilioMatch ? path.match(TWILIO_GATEWAY_WEBHOOK_RE) : null;
748
+ const gatewayTwilioMatch = !twilioMatch
749
+ ? path.match(TWILIO_GATEWAY_WEBHOOK_RE)
750
+ : null;
600
751
  const resolvedTwilioSubpath = twilioMatch
601
752
  ? twilioMatch[1]
602
753
  : gatewayTwilioMatch
603
754
  ? GATEWAY_SUBPATH_MAP[gatewayTwilioMatch[1]]
604
755
  : null;
605
- if (!resolvedTwilioSubpath || req.method !== 'POST') return null;
756
+ if (!resolvedTwilioSubpath || req.method !== "POST") return null;
606
757
 
607
758
  const twilioSubpath = resolvedTwilioSubpath;
608
759
 
609
760
  if (GATEWAY_ONLY_BLOCKED_SUBPATHS.has(twilioSubpath)) {
610
- return httpError('GONE', 'Direct webhook access disabled. Use the gateway.', 410);
761
+ return httpError(
762
+ "GONE",
763
+ "Direct webhook access disabled. Use the gateway.",
764
+ 410,
765
+ );
611
766
  }
612
767
 
613
768
  const validation = await validateTwilioWebhook(req);
@@ -615,9 +770,12 @@ export class RuntimeHttpServer {
615
770
 
616
771
  const validatedReq = cloneRequestWithBody(req, validation.body);
617
772
 
618
- if (twilioSubpath === 'voice-webhook') return await handleVoiceWebhook(validatedReq);
619
- if (twilioSubpath === 'status') return await handleStatusCallback(validatedReq);
620
- if (twilioSubpath === 'connect-action') return await handleConnectAction(validatedReq);
773
+ if (twilioSubpath === "voice-webhook")
774
+ return await handleVoiceWebhook(validatedReq);
775
+ if (twilioSubpath === "status")
776
+ return await handleStatusCallback(validatedReq);
777
+ if (twilioSubpath === "connect-action")
778
+ return await handleConnectAction(validatedReq);
621
779
 
622
780
  return null;
623
781
  }
@@ -633,61 +791,100 @@ export class RuntimeHttpServer {
633
791
  ): Promise<Response> {
634
792
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
635
793
  return withErrorHandling(endpoint, async () => {
636
- if (endpoint === 'health' && req.method === 'GET') return handleHealth();
637
- if (endpoint === 'debug' && req.method === 'GET') return handleDebug();
794
+ if (endpoint === "health" && req.method === "GET") return handleHealth();
795
+ if (endpoint === "debug" && req.method === "GET") return handleDebug();
638
796
 
639
- if (endpoint === 'browser-relay/status' && req.method === 'GET') {
797
+ if (endpoint === "browser-relay/status" && req.method === "GET") {
640
798
  return Response.json(extensionRelayServer.getStatus());
641
799
  }
642
800
 
643
- if (endpoint === 'browser-relay/command' && req.method === 'POST') {
801
+ if (endpoint === "browser-relay/command" && req.method === "POST") {
644
802
  try {
645
- const body = await req.json() as Record<string, unknown>;
646
- const resp = await extensionRelayServer.sendCommand(body as Omit<import('../browser-extension-relay/protocol.js').ExtensionCommand, 'id'>);
803
+ const body = (await req.json()) as Record<string, unknown>;
804
+ const resp = await extensionRelayServer.sendCommand(
805
+ body as Omit<
806
+ import("../browser-extension-relay/protocol.js").ExtensionCommand,
807
+ "id"
808
+ >,
809
+ );
647
810
  return Response.json(resp);
648
811
  } catch (err) {
649
- return httpError('INTERNAL_ERROR', err instanceof Error ? err.message : String(err), 500);
812
+ return httpError(
813
+ "INTERNAL_ERROR",
814
+ err instanceof Error ? err.message : String(err),
815
+ 500,
816
+ );
650
817
  }
651
818
  }
652
819
 
653
- if (endpoint === 'conversations' && req.method === 'GET') {
654
- const limit = Number(url.searchParams.get('limit') ?? 50);
655
- const offset = Number(url.searchParams.get('offset') ?? 0);
656
- const conversations = conversationStore.listConversations(limit, false, offset);
820
+ if (endpoint === "conversations" && req.method === "GET") {
821
+ const limit = Number(url.searchParams.get("limit") ?? 50);
822
+ const offset = Number(url.searchParams.get("offset") ?? 0);
823
+ const conversations = conversationStore.listConversations(
824
+ limit,
825
+ false,
826
+ offset,
827
+ );
657
828
  const totalCount = conversationStore.countConversations();
658
829
  const conversationIds = conversations.map((c) => c.id);
659
- const bindings = externalConversationStore.getBindingsForConversations(conversationIds);
660
- const attentionStates = getAttentionStateByConversationIds(conversationIds);
830
+ const bindings =
831
+ externalConversationStore.getBindingsForConversations(
832
+ conversationIds,
833
+ );
834
+ const attentionStates =
835
+ getAttentionStateByConversationIds(conversationIds);
661
836
  return Response.json({
662
837
  sessions: conversations.map((c) => {
663
838
  const binding = bindings.get(c.id);
664
839
  const originChannel = parseChannelId(c.originChannel);
665
840
  const attn = attentionStates.get(c.id);
666
- const assistantAttention = attn ? {
667
- hasUnseenLatestAssistantMessage: attn.latestAssistantMessageAt != null &&
668
- (attn.lastSeenAssistantMessageAt == null || attn.lastSeenAssistantMessageAt < attn.latestAssistantMessageAt),
669
- ...(attn.latestAssistantMessageAt != null ? { latestAssistantMessageAt: attn.latestAssistantMessageAt } : {}),
670
- ...(attn.lastSeenAssistantMessageAt != null ? { lastSeenAssistantMessageAt: attn.lastSeenAssistantMessageAt } : {}),
671
- ...(attn.lastSeenConfidence != null ? { lastSeenConfidence: attn.lastSeenConfidence } : {}),
672
- ...(attn.lastSeenSignalType != null ? { lastSeenSignalType: attn.lastSeenSignalType } : {}),
673
- } : undefined;
841
+ const assistantAttention = attn
842
+ ? {
843
+ hasUnseenLatestAssistantMessage:
844
+ attn.latestAssistantMessageAt != null &&
845
+ (attn.lastSeenAssistantMessageAt == null ||
846
+ attn.lastSeenAssistantMessageAt <
847
+ attn.latestAssistantMessageAt),
848
+ ...(attn.latestAssistantMessageAt != null
849
+ ? {
850
+ latestAssistantMessageAt: attn.latestAssistantMessageAt,
851
+ }
852
+ : {}),
853
+ ...(attn.lastSeenAssistantMessageAt != null
854
+ ? {
855
+ lastSeenAssistantMessageAt:
856
+ attn.lastSeenAssistantMessageAt,
857
+ }
858
+ : {}),
859
+ ...(attn.lastSeenConfidence != null
860
+ ? { lastSeenConfidence: attn.lastSeenConfidence }
861
+ : {}),
862
+ ...(attn.lastSeenSignalType != null
863
+ ? { lastSeenSignalType: attn.lastSeenSignalType }
864
+ : {}),
865
+ }
866
+ : undefined;
674
867
  return {
675
868
  id: c.id,
676
- title: c.title ?? 'Untitled',
869
+ title: c.title ?? "Untitled",
677
870
  createdAt: c.createdAt,
678
871
  updatedAt: c.updatedAt,
679
- threadType: c.threadType === 'private' ? 'private' : 'standard',
680
- source: c.source ?? 'user',
681
- ...(binding ? {
682
- channelBinding: {
683
- sourceChannel: binding.sourceChannel,
684
- externalChatId: binding.externalChatId,
685
- externalUserId: binding.externalUserId,
686
- displayName: binding.displayName,
687
- username: binding.username,
688
- },
689
- } : {}),
690
- ...(originChannel ? { conversationOriginChannel: originChannel } : {}),
872
+ threadType: c.threadType === "private" ? "private" : "standard",
873
+ source: c.source ?? "user",
874
+ ...(binding
875
+ ? {
876
+ channelBinding: {
877
+ sourceChannel: binding.sourceChannel,
878
+ externalChatId: binding.externalChatId,
879
+ externalUserId: binding.externalUserId,
880
+ displayName: binding.displayName,
881
+ username: binding.username,
882
+ },
883
+ }
884
+ : {}),
885
+ ...(originChannel
886
+ ? { conversationOriginChannel: originChannel }
887
+ : {}),
691
888
  ...(assistantAttention ? { assistantAttention } : {}),
692
889
  };
693
890
  }),
@@ -695,132 +892,288 @@ export class RuntimeHttpServer {
695
892
  });
696
893
  }
697
894
 
698
- if (endpoint === 'conversations/attention' && req.method === 'GET') return handleListConversationAttention(url);
895
+ if (endpoint === "conversations/attention" && req.method === "GET")
896
+ return handleListConversationAttention(url);
699
897
 
700
- if (endpoint === 'conversations/seen' && req.method === 'POST') {
701
- const body = await req.json() as Record<string, unknown>;
898
+ if (endpoint === "conversations/seen" && req.method === "POST") {
899
+ const body = (await req.json()) as Record<string, unknown>;
702
900
  const conversationId = body.conversationId as string | undefined;
703
- if (!conversationId) return httpError('BAD_REQUEST', 'Missing conversationId', 400);
901
+ if (!conversationId)
902
+ return httpError("BAD_REQUEST", "Missing conversationId", 400);
704
903
  try {
705
904
  recordConversationSeenSignal({
706
905
  conversationId,
707
906
  assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
708
- sourceChannel: (body.sourceChannel as string) ?? 'vellum',
709
- signalType: (body.signalType as string ?? 'macos_conversation_opened') as SignalType,
710
- confidence: (body.confidence as string ?? 'explicit') as Confidence,
711
- source: (body.source as string) ?? 'http-api',
907
+ sourceChannel: (body.sourceChannel as string) ?? "vellum",
908
+ signalType: ((body.signalType as string) ??
909
+ "macos_conversation_opened") as SignalType,
910
+ confidence: ((body.confidence as string) ??
911
+ "explicit") as Confidence,
912
+ source: (body.source as string) ?? "http-api",
712
913
  evidenceText: body.evidenceText as string | undefined,
713
914
  metadata: body.metadata as Record<string, unknown> | undefined,
714
915
  observedAt: body.observedAt as number | undefined,
715
916
  });
716
917
  return Response.json({ ok: true });
717
918
  } catch (err) {
718
- log.error({ err, conversationId }, 'POST /v1/conversations/seen: failed');
719
- return httpError('INTERNAL_ERROR', 'Failed to record seen signal', 500);
919
+ log.error(
920
+ { err, conversationId },
921
+ "POST /v1/conversations/seen: failed",
922
+ );
923
+ return httpError(
924
+ "INTERNAL_ERROR",
925
+ "Failed to record seen signal",
926
+ 500,
927
+ );
720
928
  }
721
929
  }
722
930
 
723
- if (endpoint === 'messages' && req.method === 'GET') return handleListMessages(url, this.interfacesDir);
724
- if (endpoint === 'search' && req.method === 'GET') return handleSearchConversations(url);
725
-
726
- if (endpoint === 'messages' && req.method === 'POST') {
727
- return await handleSendMessage(req, {
728
- processMessage: this.processMessage,
729
- persistAndProcessMessage: this.persistAndProcessMessage,
730
- sendMessageDeps: this.sendMessageDeps,
731
- approvalConversationGenerator: this.approvalConversationGenerator,
732
- }, server);
931
+ if (endpoint === "messages" && req.method === "GET")
932
+ return handleListMessages(url, this.interfacesDir);
933
+ if (endpoint === "search" && req.method === "GET")
934
+ return handleSearchConversations(url);
935
+
936
+ if (endpoint === "messages" && req.method === "POST") {
937
+ return await handleSendMessage(
938
+ req,
939
+ {
940
+ processMessage: this.processMessage,
941
+ persistAndProcessMessage: this.persistAndProcessMessage,
942
+ sendMessageDeps: this.sendMessageDeps,
943
+ approvalConversationGenerator: this.approvalConversationGenerator,
944
+ },
945
+ server,
946
+ );
733
947
  }
734
948
 
735
949
  // Standalone approval endpoints — keyed by requestId, orthogonal to message sending
736
- if (endpoint === 'confirm' && req.method === 'POST') return await handleConfirm(req, server);
737
- if (endpoint === 'secret' && req.method === 'POST') return await handleSecret(req, server);
738
- if (endpoint === 'trust-rules' && req.method === 'POST') return await handleTrustRule(req, server);
739
- if (endpoint === 'pending-interactions' && req.method === 'GET') return handleListPendingInteractions(url, req, server);
950
+ if (endpoint === "confirm" && req.method === "POST")
951
+ return await handleConfirm(req, server);
952
+ if (endpoint === "secret" && req.method === "POST")
953
+ return await handleSecret(req, server);
954
+ if (endpoint === "trust-rules" && req.method === "POST")
955
+ return await handleTrustRule(req, server);
956
+ if (endpoint === "pending-interactions" && req.method === "GET")
957
+ return handleListPendingInteractions(url, req, server);
958
+
959
+ // Trust rule CRUD — standalone management (not approval-flow)
960
+ if (endpoint === "trust-rules/manage" && req.method === "GET")
961
+ return handleListTrustRules();
962
+ if (endpoint === "trust-rules/manage" && req.method === "POST")
963
+ return await handleAddTrustRuleManage(req);
964
+ const trustRuleManageMatch = endpoint.match(
965
+ /^trust-rules\/manage\/([^/]+)$/,
966
+ );
967
+ if (trustRuleManageMatch && req.method === "DELETE")
968
+ return handleRemoveTrustRuleManage(trustRuleManageMatch[1]);
969
+ if (trustRuleManageMatch && req.method === "PATCH")
970
+ return await handleUpdateTrustRuleManage(req, trustRuleManageMatch[1]);
971
+
972
+ // Surface action dispatch
973
+ if (endpoint === "surface-actions" && req.method === "POST") {
974
+ if (!this.findSession) {
975
+ return httpError(
976
+ "NOT_IMPLEMENTED",
977
+ "Surface actions not available",
978
+ 501,
979
+ );
980
+ }
981
+ return await handleSurfaceAction(req, this.findSession);
982
+ }
740
983
 
741
984
  // Guardian action endpoints — deterministic button-based decisions
742
- if (endpoint === 'guardian-actions/pending' && req.method === 'GET') return handleGuardianActionsPending(req, server);
743
- if (endpoint === 'guardian-actions/decision' && req.method === 'POST') return await handleGuardianActionDecision(req, server);
985
+ if (endpoint === "guardian-actions/pending" && req.method === "GET")
986
+ return handleGuardianActionsPending(req, server);
987
+ if (endpoint === "guardian-actions/decision" && req.method === "POST")
988
+ return await handleGuardianActionDecision(req, server);
744
989
 
745
990
  // Contacts
746
- if (endpoint === 'contacts' && req.method === 'GET') return handleListContacts(url);
747
- if (endpoint === 'contacts/merge' && req.method === 'POST') return await handleMergeContacts(req);
991
+ if (endpoint === "contacts" && req.method === "GET")
992
+ return handleListContacts(url);
993
+ if (endpoint === "contacts/merge" && req.method === "POST")
994
+ return await handleMergeContacts(req);
748
995
  const contactMatch = endpoint.match(/^contacts\/([^/]+)$/);
749
- if (contactMatch && req.method === 'GET') return handleGetContact(contactMatch[1]);
996
+ if (contactMatch && req.method === "GET")
997
+ return handleGetContact(contactMatch[1]);
750
998
 
751
999
  // Ingress members
752
- if (endpoint === 'ingress/members' && req.method === 'GET') return handleListMembers(url);
753
- if (endpoint === 'ingress/members' && req.method === 'POST') return await handleUpsertMember(req);
754
- const memberBlockMatch = endpoint.match(/^ingress\/members\/([^/]+)\/block$/);
755
- if (memberBlockMatch && req.method === 'POST') return await handleBlockMember(req, memberBlockMatch[1]);
1000
+ if (endpoint === "ingress/members" && req.method === "GET")
1001
+ return handleListMembers(url);
1002
+ if (endpoint === "ingress/members" && req.method === "POST")
1003
+ return await handleUpsertMember(req);
1004
+ const memberBlockMatch = endpoint.match(
1005
+ /^ingress\/members\/([^/]+)\/block$/,
1006
+ );
1007
+ if (memberBlockMatch && req.method === "POST")
1008
+ return await handleBlockMember(req, memberBlockMatch[1]);
756
1009
  const memberMatch = endpoint.match(/^ingress\/members\/([^/]+)$/);
757
- if (memberMatch && req.method === 'DELETE') return await handleRevokeMember(req, memberMatch[1]);
1010
+ if (memberMatch && req.method === "DELETE")
1011
+ return await handleRevokeMember(req, memberMatch[1]);
758
1012
 
759
1013
  // Ingress invites
760
- if (endpoint === 'ingress/invites' && req.method === 'GET') return handleListInvites(url);
761
- if (endpoint === 'ingress/invites' && req.method === 'POST') return await handleCreateInvite(req);
762
- if (endpoint === 'ingress/invites/redeem' && req.method === 'POST') return await handleRedeemInvite(req);
1014
+ if (endpoint === "ingress/invites" && req.method === "GET")
1015
+ return handleListInvites(url);
1016
+ if (endpoint === "ingress/invites" && req.method === "POST")
1017
+ return await handleCreateInvite(req);
1018
+ if (endpoint === "ingress/invites/redeem" && req.method === "POST")
1019
+ return await handleRedeemInvite(req);
763
1020
  const inviteMatch = endpoint.match(/^ingress\/invites\/([^/]+)$/);
764
- if (inviteMatch && req.method === 'DELETE') return handleRevokeInvite(inviteMatch[1]);
1021
+ if (inviteMatch && req.method === "DELETE")
1022
+ return handleRevokeInvite(inviteMatch[1]);
765
1023
 
766
1024
  // Integrations — Telegram config
767
- if (endpoint === 'integrations/telegram/config' && req.method === 'GET') return handleGetTelegramConfig();
768
- if (endpoint === 'integrations/telegram/config' && req.method === 'POST') return await handleSetTelegramConfig(req);
769
- if (endpoint === 'integrations/telegram/config' && req.method === 'DELETE') return await handleClearTelegramConfig();
770
- if (endpoint === 'integrations/telegram/commands' && req.method === 'POST') return await handleSetTelegramCommands(req);
771
- if (endpoint === 'integrations/telegram/setup' && req.method === 'POST') return await handleSetupTelegram(req);
1025
+ if (endpoint === "integrations/telegram/config" && req.method === "GET")
1026
+ return handleGetTelegramConfig();
1027
+ if (endpoint === "integrations/telegram/config" && req.method === "POST")
1028
+ return await handleSetTelegramConfig(req);
1029
+ if (
1030
+ endpoint === "integrations/telegram/config" &&
1031
+ req.method === "DELETE"
1032
+ )
1033
+ return await handleClearTelegramConfig();
1034
+ if (
1035
+ endpoint === "integrations/telegram/commands" &&
1036
+ req.method === "POST"
1037
+ )
1038
+ return await handleSetTelegramCommands(req);
1039
+ if (endpoint === "integrations/telegram/setup" && req.method === "POST")
1040
+ return await handleSetupTelegram(req);
772
1041
 
773
1042
  // Integrations — Slack channel config
774
- if (endpoint === 'integrations/slack/channel/config' && req.method === 'GET') return handleGetSlackChannelConfig();
775
- if (endpoint === 'integrations/slack/channel/config' && req.method === 'POST') return await handleSetSlackChannelConfig(req);
776
- if (endpoint === 'integrations/slack/channel/config' && req.method === 'DELETE') return handleClearSlackChannelConfig();
1043
+ if (
1044
+ endpoint === "integrations/slack/channel/config" &&
1045
+ req.method === "GET"
1046
+ )
1047
+ return handleGetSlackChannelConfig();
1048
+ if (
1049
+ endpoint === "integrations/slack/channel/config" &&
1050
+ req.method === "POST"
1051
+ )
1052
+ return await handleSetSlackChannelConfig(req);
1053
+ if (
1054
+ endpoint === "integrations/slack/channel/config" &&
1055
+ req.method === "DELETE"
1056
+ )
1057
+ return handleClearSlackChannelConfig();
777
1058
 
778
1059
  // Integrations — Guardian verification
779
- if (endpoint === 'integrations/guardian/challenge' && req.method === 'POST') return await handleCreateGuardianChallenge(req);
780
- if (endpoint === 'integrations/guardian/status' && req.method === 'GET') return handleGetGuardianStatus(url);
781
- if (endpoint === 'integrations/guardian/outbound/start' && req.method === 'POST') return await handleStartOutbound(req);
782
- if (endpoint === 'integrations/guardian/outbound/resend' && req.method === 'POST') return await handleResendOutbound(req);
783
- if (endpoint === 'integrations/guardian/outbound/cancel' && req.method === 'POST') return await handleCancelOutbound(req);
1060
+ if (
1061
+ endpoint === "integrations/guardian/challenge" &&
1062
+ req.method === "POST"
1063
+ )
1064
+ return await handleCreateGuardianChallenge(req);
1065
+ if (endpoint === "integrations/guardian/status" && req.method === "GET")
1066
+ return handleGetGuardianStatus(url);
1067
+ if (
1068
+ endpoint === "integrations/guardian/outbound/start" &&
1069
+ req.method === "POST"
1070
+ )
1071
+ return await handleStartOutbound(req);
1072
+ if (
1073
+ endpoint === "integrations/guardian/outbound/resend" &&
1074
+ req.method === "POST"
1075
+ )
1076
+ return await handleResendOutbound(req);
1077
+ if (
1078
+ endpoint === "integrations/guardian/outbound/cancel" &&
1079
+ req.method === "POST"
1080
+ )
1081
+ return await handleCancelOutbound(req);
784
1082
 
785
1083
  // Guardian vellum channel bootstrap
786
- if (endpoint === 'integrations/guardian/vellum/bootstrap' && req.method === 'POST') return await handleGuardianBootstrap(req, server);
787
- if (endpoint === 'integrations/guardian/vellum/refresh' && req.method === 'POST') return await handleGuardianRefresh(req);
1084
+ if (
1085
+ endpoint === "integrations/guardian/vellum/bootstrap" &&
1086
+ req.method === "POST"
1087
+ )
1088
+ return await handleGuardianBootstrap(req, server);
1089
+ if (
1090
+ endpoint === "integrations/guardian/vellum/refresh" &&
1091
+ req.method === "POST"
1092
+ )
1093
+ return await handleGuardianRefresh(req);
788
1094
 
789
1095
  // Integrations — Twilio config
790
- if (endpoint === 'integrations/twilio/config' && req.method === 'GET') return handleGetTwilioConfig();
791
- if (endpoint === 'integrations/twilio/credentials' && req.method === 'POST') return await handleSetTwilioCredentials(req);
792
- if (endpoint === 'integrations/twilio/credentials' && req.method === 'DELETE') return handleClearTwilioCredentials();
793
- if (endpoint === 'integrations/twilio/numbers' && req.method === 'GET') return await handleListTwilioNumbers();
794
- if (endpoint === 'integrations/twilio/numbers/provision' && req.method === 'POST') return await handleProvisionTwilioNumber(req);
795
- if (endpoint === 'integrations/twilio/numbers/assign' && req.method === 'POST') return await handleAssignTwilioNumber(req);
796
- if (endpoint === 'integrations/twilio/numbers/release' && req.method === 'POST') return await handleReleaseTwilioNumber(req);
797
- if (endpoint === 'integrations/twilio/sms/compliance' && req.method === 'GET') return await handleGetSmsCompliance();
798
- if (endpoint === 'integrations/twilio/sms/compliance/tollfree' && req.method === 'POST') return await handleSubmitTollfreeVerification(req);
799
- if (endpoint === 'integrations/twilio/sms/test' && req.method === 'POST') return await handleSmsSendTest(req);
800
- if (endpoint === 'integrations/twilio/sms/doctor' && req.method === 'POST') return await handleSmsDoctor();
1096
+ if (endpoint === "integrations/twilio/config" && req.method === "GET")
1097
+ return handleGetTwilioConfig();
1098
+ if (
1099
+ endpoint === "integrations/twilio/credentials" &&
1100
+ req.method === "POST"
1101
+ )
1102
+ return await handleSetTwilioCredentials(req);
1103
+ if (
1104
+ endpoint === "integrations/twilio/credentials" &&
1105
+ req.method === "DELETE"
1106
+ )
1107
+ return handleClearTwilioCredentials();
1108
+ if (endpoint === "integrations/twilio/numbers" && req.method === "GET")
1109
+ return await handleListTwilioNumbers();
1110
+ if (
1111
+ endpoint === "integrations/twilio/numbers/provision" &&
1112
+ req.method === "POST"
1113
+ )
1114
+ return await handleProvisionTwilioNumber(req);
1115
+ if (
1116
+ endpoint === "integrations/twilio/numbers/assign" &&
1117
+ req.method === "POST"
1118
+ )
1119
+ return await handleAssignTwilioNumber(req);
1120
+ if (
1121
+ endpoint === "integrations/twilio/numbers/release" &&
1122
+ req.method === "POST"
1123
+ )
1124
+ return await handleReleaseTwilioNumber(req);
1125
+ if (
1126
+ endpoint === "integrations/twilio/sms/compliance" &&
1127
+ req.method === "GET"
1128
+ )
1129
+ return await handleGetSmsCompliance();
1130
+ if (
1131
+ endpoint === "integrations/twilio/sms/compliance/tollfree" &&
1132
+ req.method === "POST"
1133
+ )
1134
+ return await handleSubmitTollfreeVerification(req);
1135
+ if (endpoint === "integrations/twilio/sms/test" && req.method === "POST")
1136
+ return await handleSmsSendTest(req);
1137
+ if (
1138
+ endpoint === "integrations/twilio/sms/doctor" &&
1139
+ req.method === "POST"
1140
+ )
1141
+ return await handleSmsDoctor();
801
1142
 
802
1143
  // Twilio toll-free verification PATCH/DELETE with :verificationSid
803
- const tollfreeVerificationMatch = endpoint.match(/^integrations\/twilio\/sms\/compliance\/tollfree\/([^/]+)$/);
1144
+ const tollfreeVerificationMatch = endpoint.match(
1145
+ /^integrations\/twilio\/sms\/compliance\/tollfree\/([^/]+)$/,
1146
+ );
804
1147
  if (tollfreeVerificationMatch) {
805
1148
  const verificationSid = tollfreeVerificationMatch[1];
806
- if (req.method === 'PATCH') return await handleUpdateTollfreeVerification(req, verificationSid);
807
- if (req.method === 'DELETE') return await handleDeleteTollfreeVerification(verificationSid);
1149
+ if (req.method === "PATCH")
1150
+ return await handleUpdateTollfreeVerification(req, verificationSid);
1151
+ if (req.method === "DELETE")
1152
+ return await handleDeleteTollfreeVerification(verificationSid);
808
1153
  }
809
1154
 
810
1155
  // Channel readiness
811
- if (endpoint === 'channels/readiness' && req.method === 'GET') return await handleGetChannelReadiness(url);
812
- if (endpoint === 'channels/readiness/refresh' && req.method === 'POST') return await handleRefreshChannelReadiness(req);
813
-
814
- if (endpoint === 'attachments' && req.method === 'POST') return await handleUploadAttachment(req);
815
- if (endpoint === 'attachments' && req.method === 'DELETE') return await handleDeleteAttachment(req);
816
-
817
- const attachmentContentMatch = endpoint.match(/^attachments\/([^/]+)\/content$/);
818
- if (attachmentContentMatch && req.method === 'GET') return handleGetAttachmentContent(attachmentContentMatch[1], req);
1156
+ if (endpoint === "channels/readiness" && req.method === "GET")
1157
+ return await handleGetChannelReadiness(url);
1158
+ if (endpoint === "channels/readiness/refresh" && req.method === "POST")
1159
+ return await handleRefreshChannelReadiness(req);
1160
+
1161
+ if (endpoint === "attachments" && req.method === "POST")
1162
+ return await handleUploadAttachment(req);
1163
+ if (endpoint === "attachments" && req.method === "DELETE")
1164
+ return await handleDeleteAttachment(req);
1165
+
1166
+ const attachmentContentMatch = endpoint.match(
1167
+ /^attachments\/([^/]+)\/content$/,
1168
+ );
1169
+ if (attachmentContentMatch && req.method === "GET")
1170
+ return handleGetAttachmentContent(attachmentContentMatch[1], req);
819
1171
 
820
1172
  const attachmentMatch = endpoint.match(/^attachments\/([^/]+)$/);
821
- if (attachmentMatch && req.method === 'GET') return handleGetAttachment(attachmentMatch[1]);
1173
+ if (attachmentMatch && req.method === "GET")
1174
+ return handleGetAttachment(attachmentMatch[1]);
822
1175
 
823
- if (endpoint === 'suggestion' && req.method === 'GET') {
1176
+ if (endpoint === "suggestion" && req.method === "GET") {
824
1177
  return await handleGetSuggestion(url, {
825
1178
  suggestionCache: this.suggestionCache,
826
1179
  suggestionInFlight: this.suggestionInFlight,
@@ -828,94 +1181,156 @@ export class RuntimeHttpServer {
828
1181
  }
829
1182
 
830
1183
  const interfacesMatch = endpoint.match(/^interfaces\/(.+)$/);
831
- if (interfacesMatch && req.method === 'GET') return this.handleGetInterface(interfacesMatch[1]);
1184
+ if (interfacesMatch && req.method === "GET")
1185
+ return this.handleGetInterface(interfacesMatch[1]);
832
1186
 
833
- if (endpoint === 'channels/conversation' && req.method === 'DELETE') return await handleDeleteConversation(req, assistantId);
1187
+ if (endpoint === "channels/conversation" && req.method === "DELETE")
1188
+ return await handleDeleteConversation(req, assistantId);
834
1189
 
835
- if (endpoint === 'channels/inbound' && req.method === 'POST') {
1190
+ if (endpoint === "channels/inbound" && req.method === "POST") {
836
1191
  const gatewayOriginSecret = getRuntimeGatewayOriginSecret();
837
- return await handleChannelInbound(req, this.processMessage, this.bearerToken, assistantId, gatewayOriginSecret, this.approvalCopyGenerator, this.approvalConversationGenerator, this.guardianActionCopyGenerator, this.guardianFollowUpConversationGenerator);
1192
+ return await handleChannelInbound(
1193
+ req,
1194
+ this.processMessage,
1195
+ this.bearerToken,
1196
+ assistantId,
1197
+ gatewayOriginSecret,
1198
+ this.approvalCopyGenerator,
1199
+ this.approvalConversationGenerator,
1200
+ this.guardianActionCopyGenerator,
1201
+ this.guardianFollowUpConversationGenerator,
1202
+ );
838
1203
  }
839
1204
 
840
- if (endpoint === 'channels/delivery-ack' && req.method === 'POST') return await handleChannelDeliveryAck(req);
841
- if (endpoint === 'channels/dead-letters' && req.method === 'GET') return handleListDeadLetters();
842
- if (endpoint === 'channels/replay' && req.method === 'POST') return await handleReplayDeadLetters(req);
1205
+ if (endpoint === "channels/delivery-ack" && req.method === "POST")
1206
+ return await handleChannelDeliveryAck(req);
1207
+ if (endpoint === "channels/dead-letters" && req.method === "GET")
1208
+ return handleListDeadLetters();
1209
+ if (endpoint === "channels/replay" && req.method === "POST")
1210
+ return await handleReplayDeadLetters(req);
843
1211
 
844
- if (endpoint === 'calls/start' && req.method === 'POST') return await handleStartCall(req, assistantId);
1212
+ if (endpoint === "calls/start" && req.method === "POST")
1213
+ return await handleStartCall(req, assistantId);
845
1214
 
846
- const callsMatch = endpoint.match(/^calls\/([^/]+?)(\/cancel|\/answer|\/instruction)?$/);
1215
+ const callsMatch = endpoint.match(
1216
+ /^calls\/([^/]+?)(\/cancel|\/answer|\/instruction)?$/,
1217
+ );
847
1218
  if (callsMatch) {
848
1219
  const callSessionId = callsMatch[1];
849
- if (callSessionId !== 'twilio' && callSessionId !== 'relay' && callSessionId !== 'start') {
850
- if (callsMatch[2] === '/cancel' && req.method === 'POST') return await handleCancelCall(req, callSessionId);
851
- if (callsMatch[2] === '/answer' && req.method === 'POST') return await handleAnswerCall(req, callSessionId);
852
- if (callsMatch[2] === '/instruction' && req.method === 'POST') return await handleInstructionCall(req, callSessionId);
853
- if (!callsMatch[2] && req.method === 'GET') return handleGetCallStatus(callSessionId);
1220
+ if (
1221
+ callSessionId !== "twilio" &&
1222
+ callSessionId !== "relay" &&
1223
+ callSessionId !== "start"
1224
+ ) {
1225
+ if (callsMatch[2] === "/cancel" && req.method === "POST")
1226
+ return await handleCancelCall(req, callSessionId);
1227
+ if (callsMatch[2] === "/answer" && req.method === "POST")
1228
+ return await handleAnswerCall(req, callSessionId);
1229
+ if (callsMatch[2] === "/instruction" && req.method === "POST")
1230
+ return await handleInstructionCall(req, callSessionId);
1231
+ if (!callsMatch[2] && req.method === "GET")
1232
+ return handleGetCallStatus(callSessionId);
854
1233
  }
855
1234
  }
856
1235
 
857
1236
  // Internal Twilio forwarding endpoints (gateway -> runtime)
858
- if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
859
- const json = await req.json() as { params: Record<string, string>; originalUrl?: string };
1237
+ if (
1238
+ endpoint === "internal/twilio/voice-webhook" &&
1239
+ req.method === "POST"
1240
+ ) {
1241
+ const json = (await req.json()) as {
1242
+ params: Record<string, string>;
1243
+ originalUrl?: string;
1244
+ };
860
1245
  const formBody = new URLSearchParams(json.params).toString();
861
1246
  const reconstructedUrl = json.originalUrl ?? req.url;
862
- const fakeReq = new Request(reconstructedUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
1247
+ const fakeReq = new Request(reconstructedUrl, {
1248
+ method: "POST",
1249
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1250
+ body: formBody,
1251
+ });
863
1252
  return await handleVoiceWebhook(fakeReq);
864
1253
  }
865
1254
 
866
- if (endpoint === 'internal/twilio/status' && req.method === 'POST') {
867
- const json = await req.json() as { params: Record<string, string> };
1255
+ if (endpoint === "internal/twilio/status" && req.method === "POST") {
1256
+ const json = (await req.json()) as { params: Record<string, string> };
868
1257
  const formBody = new URLSearchParams(json.params).toString();
869
- const fakeReq = new Request(req.url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
1258
+ const fakeReq = new Request(req.url, {
1259
+ method: "POST",
1260
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1261
+ body: formBody,
1262
+ });
870
1263
  return await handleStatusCallback(fakeReq);
871
1264
  }
872
1265
 
873
- if (endpoint === 'internal/twilio/connect-action' && req.method === 'POST') {
874
- const json = await req.json() as { params: Record<string, string> };
1266
+ if (
1267
+ endpoint === "internal/twilio/connect-action" &&
1268
+ req.method === "POST"
1269
+ ) {
1270
+ const json = (await req.json()) as { params: Record<string, string> };
875
1271
  const formBody = new URLSearchParams(json.params).toString();
876
- const fakeReq = new Request(req.url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
1272
+ const fakeReq = new Request(req.url, {
1273
+ method: "POST",
1274
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1275
+ body: formBody,
1276
+ });
877
1277
  return await handleConnectAction(fakeReq);
878
1278
  }
879
1279
 
880
- if (endpoint === 'identity' && req.method === 'GET') return handleGetIdentity();
881
- if (endpoint === 'brain-graph' && req.method === 'GET') return handleGetBrainGraph();
882
- if (endpoint === 'brain-graph-ui' && req.method === 'GET') return handleServeBrainGraphUI(this.bearerToken);
883
- if (endpoint === 'home-base-ui' && req.method === 'GET') return handleServeHomeBaseUI(this.bearerToken);
884
- if (endpoint === 'events' && req.method === 'GET') return handleSubscribeAssistantEvents(req, url, { server });
1280
+ if (endpoint === "identity" && req.method === "GET")
1281
+ return handleGetIdentity();
1282
+ if (endpoint === "brain-graph" && req.method === "GET")
1283
+ return handleGetBrainGraph();
1284
+ if (endpoint === "brain-graph-ui" && req.method === "GET")
1285
+ return handleServeBrainGraphUI(this.bearerToken);
1286
+ if (endpoint === "home-base-ui" && req.method === "GET")
1287
+ return handleServeHomeBaseUI(this.bearerToken);
1288
+ if (endpoint === "events" && req.method === "GET")
1289
+ return handleSubscribeAssistantEvents(req, url, { server });
885
1290
 
886
1291
  // Internal OAuth callback endpoint (gateway -> runtime)
887
- if (endpoint === 'internal/oauth/callback' && req.method === 'POST') {
888
- const json = await req.json() as { state: string; code?: string; error?: string };
889
- if (!json.state) return httpError('BAD_REQUEST', 'Missing state parameter', 400);
1292
+ if (endpoint === "internal/oauth/callback" && req.method === "POST") {
1293
+ const json = (await req.json()) as {
1294
+ state: string;
1295
+ code?: string;
1296
+ error?: string;
1297
+ };
1298
+ if (!json.state)
1299
+ return httpError("BAD_REQUEST", "Missing state parameter", 400);
890
1300
  if (json.error) {
891
1301
  const consumed = consumeCallbackError(json.state, json.error);
892
- return consumed ? Response.json({ ok: true }) : httpError('NOT_FOUND', 'Unknown state', 404);
1302
+ return consumed
1303
+ ? Response.json({ ok: true })
1304
+ : httpError("NOT_FOUND", "Unknown state", 404);
893
1305
  }
894
1306
  if (json.code) {
895
1307
  const consumed = consumeCallback(json.state, json.code);
896
- return consumed ? Response.json({ ok: true }) : httpError('NOT_FOUND', 'Unknown state', 404);
1308
+ return consumed
1309
+ ? Response.json({ ok: true })
1310
+ : httpError("NOT_FOUND", "Unknown state", 404);
897
1311
  }
898
- return httpError('BAD_REQUEST', 'Missing code or error parameter', 400);
1312
+ return httpError("BAD_REQUEST", "Missing code or error parameter", 400);
899
1313
  }
900
1314
 
901
- return httpError('NOT_FOUND', 'Not found', 404);
1315
+ return httpError("NOT_FOUND", "Not found", 404);
902
1316
  });
903
1317
  }
904
1318
 
905
1319
  private handleGetInterface(interfacePath: string): Response {
906
1320
  if (!this.interfacesDir) {
907
- return httpError('NOT_FOUND', 'Interface not found', 404);
1321
+ return httpError("NOT_FOUND", "Interface not found", 404);
908
1322
  }
909
1323
  const fullPath = resolve(this.interfacesDir, interfacePath);
910
1324
  if (
911
- (fullPath !== this.interfacesDir && !fullPath.startsWith(this.interfacesDir + '/')) ||
1325
+ (fullPath !== this.interfacesDir &&
1326
+ !fullPath.startsWith(this.interfacesDir + "/")) ||
912
1327
  !existsSync(fullPath)
913
1328
  ) {
914
- return httpError('NOT_FOUND', 'Interface not found', 404);
1329
+ return httpError("NOT_FOUND", "Interface not found", 404);
915
1330
  }
916
- const source = readFileSync(fullPath, 'utf-8');
1331
+ const source = readFileSync(fullPath, "utf-8");
917
1332
  return new Response(source, {
918
- headers: { 'Content-Type': 'text/plain; charset=utf-8' },
1333
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
919
1334
  });
920
1335
  }
921
1336
  }