@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.
- package/ARCHITECTURE.md +401 -385
- package/package.json +1 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
- package/src/__tests__/registry.test.ts +235 -187
- package/src/__tests__/secure-keys.test.ts +27 -0
- package/src/__tests__/session-agent-loop.test.ts +521 -256
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/skills.test.ts +334 -276
- package/src/__tests__/slack-skill.test.ts +124 -0
- package/src/__tests__/starter-task-flow.test.ts +7 -17
- package/src/agent/loop.ts +10 -3
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
- package/src/config/bundled-skills/doordash/SKILL.md +171 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
- package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
- package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
- package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
- package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
- package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
- package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
- package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
- package/src/config/bundled-skills/messaging/SKILL.md +59 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
- package/src/config/bundled-skills/notion/SKILL.md +240 -0
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
- package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
- package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
- package/src/config/bundled-skills/slack/SKILL.md +49 -0
- package/src/config/bundled-skills/slack/TOOLS.json +167 -0
- package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
- package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
- package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
- package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
- package/src/config/bundled-tool-registry.ts +292 -267
- package/src/config/schema.ts +1 -1
- package/src/daemon/handlers/skills.ts +334 -234
- package/src/daemon/ipc-contract/messages.ts +2 -0
- package/src/daemon/ipc-contract/surfaces.ts +2 -0
- package/src/daemon/lifecycle.ts +358 -221
- package/src/daemon/response-tier.ts +2 -0
- package/src/daemon/server.ts +453 -193
- package/src/daemon/session-agent-loop-handlers.ts +43 -2
- package/src/daemon/session-agent-loop.ts +3 -0
- package/src/daemon/session-lifecycle.ts +3 -0
- package/src/daemon/session-process.ts +1 -0
- package/src/daemon/session-surfaces.ts +22 -20
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +5 -2
- package/src/messaging/outreach-classifier.ts +12 -5
- package/src/messaging/provider-types.ts +5 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +11 -5
- package/src/messaging/providers/gmail/client.ts +2 -0
- package/src/messaging/providers/slack/adapter.ts +1 -0
- package/src/messaging/providers/slack/client.ts +8 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/runtime/http-errors.ts +33 -20
- package/src/runtime/http-server.ts +706 -291
- package/src/runtime/http-types.ts +26 -16
- package/src/runtime/routes/secret-routes.ts +57 -2
- package/src/runtime/routes/surface-action-routes.ts +66 -0
- package/src/runtime/routes/trust-rules-routes.ts +140 -0
- package/src/security/keychain-to-encrypted-migration.ts +59 -0
- package/src/security/secure-keys.ts +17 -0
- package/src/skills/frontmatter.ts +9 -7
- package/src/tools/apps/executors.ts +2 -1
- package/src/tools/tool-manifest.ts +44 -42
- package/src/tools/types.ts +9 -0
- package/src/__tests__/skill-mirror-parity.test.ts +0 -176
- package/src/config/vellum-skills/catalog.json +0 -63
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
- package/src/skills/vellum-catalog-remote.ts +0 -166
- package/src/tools/skills/vellum-catalog.ts +0 -168
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
- /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
|
|
9
|
-
import { homedir } from
|
|
10
|
-
import { join, resolve } from
|
|
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
|
|
12
|
+
import type { ServerWebSocket } from "bun";
|
|
13
13
|
|
|
14
|
-
import type { BrowserRelayWebSocketData } from
|
|
15
|
-
import { extensionRelayServer } from
|
|
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
|
|
20
|
-
import type { RelayWebSocketData } from
|
|
21
|
-
import {
|
|
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
|
|
27
|
-
import { parseChannelId } from
|
|
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
|
|
34
|
-
import type { ServerMessage } from
|
|
35
|
-
import { PairingStore } from
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
import
|
|
43
|
-
import
|
|
44
|
-
import {
|
|
45
|
-
|
|
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
|
|
54
|
-
import { withErrorHandling } from
|
|
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
|
|
62
|
-
import { withRequestLogging } from
|
|
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
|
|
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
|
|
88
|
+
} from "./routes/app-routes.js";
|
|
78
89
|
import {
|
|
79
90
|
handleConfirm,
|
|
80
91
|
handleListPendingInteractions,
|
|
81
92
|
handleSecret,
|
|
82
93
|
handleTrustRule,
|
|
83
|
-
} from
|
|
94
|
+
} from "./routes/approval-routes.js";
|
|
84
95
|
import {
|
|
85
96
|
handleDeleteAttachment,
|
|
86
97
|
handleGetAttachment,
|
|
87
98
|
handleGetAttachmentContent,
|
|
88
99
|
handleUploadAttachment,
|
|
89
|
-
} from
|
|
90
|
-
import {
|
|
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
|
|
112
|
+
} from "./routes/call-routes.js";
|
|
98
113
|
import {
|
|
99
114
|
startCanonicalGuardianExpirySweep,
|
|
100
115
|
stopCanonicalGuardianExpirySweep,
|
|
101
|
-
} from
|
|
116
|
+
} from "./routes/canonical-guardian-expiry-sweep.js";
|
|
102
117
|
import {
|
|
103
118
|
handleGetChannelReadiness,
|
|
104
119
|
handleRefreshChannelReadiness,
|
|
105
|
-
} from
|
|
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
|
|
129
|
+
} from "./routes/channel-routes.js";
|
|
115
130
|
import {
|
|
116
131
|
handleGetContact,
|
|
117
132
|
handleListContacts,
|
|
118
133
|
handleMergeContacts,
|
|
119
|
-
} from
|
|
120
|
-
import { handleListConversationAttention } from
|
|
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
|
|
128
|
-
import { handleDebug } from
|
|
129
|
-
import { handleSubscribeAssistantEvents } from
|
|
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
|
|
134
|
-
import { handleGuardianBootstrap } from
|
|
135
|
-
import { handleGuardianRefresh } from
|
|
136
|
-
import { handleGetIdentity,handleHealth } from
|
|
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
|
|
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
|
|
162
|
-
import type { PairingHandlerContext } from
|
|
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
|
|
169
|
-
import { handleAddSecret } from
|
|
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
|
|
206
|
+
} from "./routes/twilio-routes.js";
|
|
185
207
|
|
|
186
208
|
// Re-export for consumers
|
|
187
|
-
export { isPrivateAddress } from
|
|
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
|
|
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
|
|
234
|
+
} from "./http-types.js";
|
|
213
235
|
|
|
214
|
-
const log = getLogger(
|
|
236
|
+
const log = getLogger("runtime-http");
|
|
215
237
|
|
|
216
238
|
const DEFAULT_PORT = 7821;
|
|
217
|
-
const DEFAULT_HOSTNAME =
|
|
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 =
|
|
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,
|
|
276
|
-
const token = readFileSync(tokenPath,
|
|
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(
|
|
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 (
|
|
313
|
-
extensionRelayServer.handleOpen(
|
|
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 },
|
|
348
|
+
log.info({ callSessionId }, "ConversationRelay WebSocket opened");
|
|
318
349
|
if (callSessionId) {
|
|
319
|
-
const connection = new RelayConnection(
|
|
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 =
|
|
326
|
-
|
|
327
|
-
|
|
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 (
|
|
339
|
-
extensionRelayServer.handleClose(
|
|
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(
|
|
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(() => {
|
|
407
|
+
sweepFailedEvents(pm, bt).finally(() => {
|
|
408
|
+
this.sweepInProgress = false;
|
|
409
|
+
});
|
|
361
410
|
}, 30_000);
|
|
362
411
|
}
|
|
363
412
|
|
|
364
|
-
startGuardianExpirySweep(
|
|
365
|
-
|
|
413
|
+
startGuardianExpirySweep(
|
|
414
|
+
getGatewayInternalBaseUrl(),
|
|
415
|
+
this.bearerToken,
|
|
416
|
+
this.approvalCopyGenerator,
|
|
417
|
+
);
|
|
418
|
+
log.info("Guardian approval expiry sweep started");
|
|
366
419
|
|
|
367
|
-
startGuardianActionSweep(
|
|
368
|
-
|
|
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(
|
|
428
|
+
log.info("Canonical guardian request expiry sweep started");
|
|
372
429
|
|
|
373
|
-
log.info(
|
|
430
|
+
log.info(
|
|
431
|
+
"Running in gateway-only ingress mode. Direct webhook routes disabled.",
|
|
432
|
+
);
|
|
374
433
|
if (!isLoopbackHost(this.hostname)) {
|
|
375
|
-
log.warn(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
473
|
+
log.info("Runtime HTTP server stopped");
|
|
402
474
|
}
|
|
403
475
|
}
|
|
404
476
|
|
|
405
|
-
private async handleRequest(
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
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 (
|
|
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 (
|
|
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 ===
|
|
524
|
+
if (path === "/v1/pairing/request" && req.method === "POST") {
|
|
441
525
|
return await handlePairingRequest(req, this.pairingContext);
|
|
442
526
|
}
|
|
443
|
-
if (path ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
590
|
+
if (pagesMatch && req.method === "GET") {
|
|
497
591
|
try {
|
|
498
592
|
return handleServePage(pagesMatch[1]);
|
|
499
593
|
} catch (err) {
|
|
500
|
-
log.error(
|
|
501
|
-
|
|
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 ===
|
|
507
|
-
try {
|
|
508
|
-
|
|
509
|
-
|
|
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 ===
|
|
517
|
-
try {
|
|
518
|
-
|
|
519
|
-
|
|
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 ===
|
|
523
|
-
try {
|
|
524
|
-
|
|
525
|
-
|
|
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(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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 ===
|
|
540
|
-
try {
|
|
541
|
-
|
|
542
|
-
|
|
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(
|
|
678
|
+
return httpError("NOT_FOUND", "Not found", 404);
|
|
553
679
|
}
|
|
554
680
|
|
|
555
|
-
private handleBrowserRelayUpgrade(
|
|
556
|
-
|
|
557
|
-
|
|
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(
|
|
698
|
+
const token = wsUrl.searchParams.get("token");
|
|
563
699
|
if (!token || !verifyBearerToken(token, this.bearerToken)) {
|
|
564
|
-
return httpError(
|
|
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: {
|
|
706
|
+
data: {
|
|
707
|
+
wsType: "browser-relay",
|
|
708
|
+
connectionId,
|
|
709
|
+
} satisfies BrowserRelayWebSocketData,
|
|
571
710
|
});
|
|
572
711
|
if (!upgraded) {
|
|
573
|
-
return new Response(
|
|
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(
|
|
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(
|
|
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(
|
|
731
|
+
const callSessionId = wsUrl.searchParams.get("callSessionId");
|
|
586
732
|
if (!callSessionId) {
|
|
587
|
-
return new Response(
|
|
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(
|
|
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(
|
|
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
|
|
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 !==
|
|
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(
|
|
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 ===
|
|
619
|
-
|
|
620
|
-
if (twilioSubpath ===
|
|
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 ===
|
|
637
|
-
if (endpoint ===
|
|
794
|
+
if (endpoint === "health" && req.method === "GET") return handleHealth();
|
|
795
|
+
if (endpoint === "debug" && req.method === "GET") return handleDebug();
|
|
638
796
|
|
|
639
|
-
if (endpoint ===
|
|
797
|
+
if (endpoint === "browser-relay/status" && req.method === "GET") {
|
|
640
798
|
return Response.json(extensionRelayServer.getStatus());
|
|
641
799
|
}
|
|
642
800
|
|
|
643
|
-
if (endpoint ===
|
|
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(
|
|
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(
|
|
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 ===
|
|
654
|
-
const limit = Number(url.searchParams.get(
|
|
655
|
-
const offset = Number(url.searchParams.get(
|
|
656
|
-
const conversations = conversationStore.listConversations(
|
|
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 =
|
|
660
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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 ??
|
|
869
|
+
title: c.title ?? "Untitled",
|
|
677
870
|
createdAt: c.createdAt,
|
|
678
871
|
updatedAt: c.updatedAt,
|
|
679
|
-
threadType: c.threadType ===
|
|
680
|
-
source: c.source ??
|
|
681
|
-
...(binding
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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 ===
|
|
895
|
+
if (endpoint === "conversations/attention" && req.method === "GET")
|
|
896
|
+
return handleListConversationAttention(url);
|
|
699
897
|
|
|
700
|
-
if (endpoint ===
|
|
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)
|
|
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) ??
|
|
709
|
-
signalType: (body.signalType as string ??
|
|
710
|
-
|
|
711
|
-
|
|
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(
|
|
719
|
-
|
|
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 ===
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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 ===
|
|
737
|
-
|
|
738
|
-
if (endpoint ===
|
|
739
|
-
|
|
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 ===
|
|
743
|
-
|
|
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 ===
|
|
747
|
-
|
|
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 ===
|
|
996
|
+
if (contactMatch && req.method === "GET")
|
|
997
|
+
return handleGetContact(contactMatch[1]);
|
|
750
998
|
|
|
751
999
|
// Ingress members
|
|
752
|
-
if (endpoint ===
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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 ===
|
|
1010
|
+
if (memberMatch && req.method === "DELETE")
|
|
1011
|
+
return await handleRevokeMember(req, memberMatch[1]);
|
|
758
1012
|
|
|
759
1013
|
// Ingress invites
|
|
760
|
-
if (endpoint ===
|
|
761
|
-
|
|
762
|
-
if (endpoint ===
|
|
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 ===
|
|
1021
|
+
if (inviteMatch && req.method === "DELETE")
|
|
1022
|
+
return handleRevokeInvite(inviteMatch[1]);
|
|
765
1023
|
|
|
766
1024
|
// Integrations — Telegram config
|
|
767
|
-
if (endpoint ===
|
|
768
|
-
|
|
769
|
-
if (endpoint ===
|
|
770
|
-
|
|
771
|
-
if (
|
|
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 (
|
|
775
|
-
|
|
776
|
-
|
|
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 (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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 (
|
|
787
|
-
|
|
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 ===
|
|
791
|
-
|
|
792
|
-
if (
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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(
|
|
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 ===
|
|
807
|
-
|
|
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 ===
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (
|
|
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 ===
|
|
1173
|
+
if (attachmentMatch && req.method === "GET")
|
|
1174
|
+
return handleGetAttachment(attachmentMatch[1]);
|
|
822
1175
|
|
|
823
|
-
if (endpoint ===
|
|
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 ===
|
|
1184
|
+
if (interfacesMatch && req.method === "GET")
|
|
1185
|
+
return this.handleGetInterface(interfacesMatch[1]);
|
|
832
1186
|
|
|
833
|
-
if (endpoint ===
|
|
1187
|
+
if (endpoint === "channels/conversation" && req.method === "DELETE")
|
|
1188
|
+
return await handleDeleteConversation(req, assistantId);
|
|
834
1189
|
|
|
835
|
-
if (endpoint ===
|
|
1190
|
+
if (endpoint === "channels/inbound" && req.method === "POST") {
|
|
836
1191
|
const gatewayOriginSecret = getRuntimeGatewayOriginSecret();
|
|
837
|
-
return await handleChannelInbound(
|
|
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 ===
|
|
841
|
-
|
|
842
|
-
if (endpoint ===
|
|
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 ===
|
|
1212
|
+
if (endpoint === "calls/start" && req.method === "POST")
|
|
1213
|
+
return await handleStartCall(req, assistantId);
|
|
845
1214
|
|
|
846
|
-
const callsMatch = endpoint.match(
|
|
1215
|
+
const callsMatch = endpoint.match(
|
|
1216
|
+
/^calls\/([^/]+?)(\/cancel|\/answer|\/instruction)?$/,
|
|
1217
|
+
);
|
|
847
1218
|
if (callsMatch) {
|
|
848
1219
|
const callSessionId = callsMatch[1];
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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 (
|
|
859
|
-
|
|
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, {
|
|
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 ===
|
|
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, {
|
|
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 (
|
|
874
|
-
|
|
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, {
|
|
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 ===
|
|
881
|
-
|
|
882
|
-
if (endpoint ===
|
|
883
|
-
|
|
884
|
-
if (endpoint ===
|
|
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 ===
|
|
888
|
-
const json = await req.json() as {
|
|
889
|
-
|
|
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
|
|
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
|
|
1308
|
+
return consumed
|
|
1309
|
+
? Response.json({ ok: true })
|
|
1310
|
+
: httpError("NOT_FOUND", "Unknown state", 404);
|
|
897
1311
|
}
|
|
898
|
-
return httpError(
|
|
1312
|
+
return httpError("BAD_REQUEST", "Missing code or error parameter", 400);
|
|
899
1313
|
}
|
|
900
1314
|
|
|
901
|
-
return httpError(
|
|
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(
|
|
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 &&
|
|
1325
|
+
(fullPath !== this.interfacesDir &&
|
|
1326
|
+
!fullPath.startsWith(this.interfacesDir + "/")) ||
|
|
912
1327
|
!existsSync(fullPath)
|
|
913
1328
|
) {
|
|
914
|
-
return httpError(
|
|
1329
|
+
return httpError("NOT_FOUND", "Interface not found", 404);
|
|
915
1330
|
}
|
|
916
|
-
const source = readFileSync(fullPath,
|
|
1331
|
+
const source = readFileSync(fullPath, "utf-8");
|
|
917
1332
|
return new Response(source, {
|
|
918
|
-
headers: {
|
|
1333
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
919
1334
|
});
|
|
920
1335
|
}
|
|
921
1336
|
}
|