| @@ -36,7 +36,7 @@ · 35 unmodified lines |
| 36 | import { Result } from "better-result"; | 36 | import { Result } from "better-result"; |
| 37 | import { WorkerEntrypoint } from "cloudflare:workers"; | 37 | import { WorkerEntrypoint } from "cloudflare:workers"; |
| 38 | import { api } from "@executor/convex/_generated/api"; | 38 | import { api } from "@executor/convex/_generated/api"; |
| 39 | import { ConvexHttpClient } from "convex/browser"; | 39 | import { ConvexClient, ConvexHttpClient } from "convex/browser"; |
| 40 | | 40 | |
| 41 | // Import isolate modules as raw text — these are loaded as JS modules inside | 41 | // Import isolate modules as raw text — these are loaded as JS modules inside |
| 42 | // the dynamic isolate, NOT executed in the host worker. The *.isolate.js | 42 | // the dynamic isolate, NOT executed in the host worker. The *.isolate.js |
| @@ -113,6 +113,8 @@ interface BridgeProps { · 70 unmodified lines |
| 113 | taskId: string; | 113 | taskId: string; |
| 114 | } | 114 | } |
| 115 | | 115 | |
| | 116 | const APPROVAL_SUBSCRIPTION_TIMEOUT_MS = 10 * 60 * 1000; |
| | 117 | |
| 116 | // ── Auth ────────────────────────────────────────────────────────────────────── | 118 | // ── Auth ────────────────────────────────────────────────────────────────────── |
| 117 | | 119 | |
| 118 | /** Constant-time string comparison to prevent timing side-channels. */ | 120 | /** Constant-time string comparison to prevent timing side-channels. */ |
| @@ -168,6 +170,58 @@ export class ToolBridge extends WorkerEntrypoint<Env> { · 49 unmodified lines |
| 168 | }); | 170 | }); |
| 169 | } | 171 | } |
| 170 | | 172 | |
| | 173 | private createRealtimeClient(): ConvexClient { |
| | 174 | return new ConvexClient(this.props.callbackConvexUrl, { |
| | 175 | skipConvexDeploymentUrlCheck: true, |
| | 176 | }); |
| | 177 | } |
| | 178 | |
| | 179 | private async waitForApprovalUpdate(approvalId: string): Promise<void> { |
| | 180 | const client = this.createRealtimeClient(); |
| | 181 | |
| | 182 | await new Promise<void>((resolve, reject) => { |
| | 183 | let settled = false; |
| | 184 | |
| | 185 | const timer = setTimeout(() => { |
| | 186 | if (settled) return; |
| | 187 | settled = true; |
| | 188 | unsubscribe(); |
| | 189 | client.close(); |
| | 190 | reject(new Error(`Timed out waiting for approval update: ${approvalId}`)); |
| | 191 | }, APPROVAL_SUBSCRIPTION_TIMEOUT_MS); |
| | 192 | |
| | 193 | const unsubscribe = client.onUpdate( |
| | 194 | (api as any).runtimeCallbacks.getApprovalStatus, |
| | 195 | { |
| | 196 | internalSecret: this.props.callbackInternalSecret, |
| | 197 | runId: this.props.taskId, |
| | 198 | approvalId, |
| | 199 | }, |
| | 200 | (value: { status?: "pending" | "approved" | "denied" | "missing" } | null | undefined) => { |
| | 201 | const status = value?.status; |
| | 202 | if (!status || status === "pending") { |
| | 203 | return; |
| | 204 | } |
| | 205 | if (status === "missing") { |
| | 206 | if (settled) return; |
| | 207 | settled = true; |
| | 208 | clearTimeout(timer); |
| | 209 | unsubscribe(); |
| | 210 | client.close(); |
| | 211 | reject(new Error(`Approval not found: ${approvalId}`)); |
| | 212 | return; |
| | 213 | } |
| | 214 | if (settled) return; |
| | 215 | settled = true; |
| | 216 | clearTimeout(timer); |
| | 217 | unsubscribe(); |
| | 218 | client.close(); |
| | 219 | resolve(); |
| | 220 | }, |
| | 221 | ); |
| | 222 | }); |
| | 223 | } |
| | 224 | |
| 171 | /** Forward a tool call to the Convex callback RPC action. */ | 225 | /** Forward a tool call to the Convex callback RPC action. */ |
| 172 | async callTool(toolPath: string, input: unknown, callId?: string): Promise<ToolCallResult> { | 226 | async callTool(toolPath: string, input: unknown, callId?: string): Promise<ToolCallResult> { |
| 173 | const { callbackInternalSecret, taskId } = this.props; | 227 | const { callbackInternalSecret, taskId } = this.props; |
| @@ -175,24 +229,42 @@ export class ToolBridge extends WorkerEntrypoint<Env> { · 1 unmodified line |
| 175 | ? callId | 229 | ? callId |
| 176 | : `call_${crypto.randomUUID()}`; | 230 | : `call_${crypto.randomUUID()}`; |
| 177 | | 231 | |
| 178 | const response = await Result.tryPromise(async () => { | 232 | while (true) { |
| 179 | const convex = this.createConvexClient(); | 233 | const response = await Result.tryPromise(async () => { |
| 180 | return await convex.action(api.runtimeCallbacks.handleToolCall, { | 234 | const convex = this.createConvexClient(); |
| 181 | internalSecret: callbackInternalSecret, | 235 | return await convex.action(api.runtimeCallbacks.handleToolCall, { |
| 182 | runId: taskId, | 236 | internalSecret: callbackInternalSecret, |
| 183 | callId: effectiveCallId, | 237 | runId: taskId, |
| 184 | toolPath, | 238 | callId: effectiveCallId, |
| 185 | input, | 239 | toolPath, |
| | 240 | input, |
| | 241 | }); |
| 186 | }); | 242 | }); |
| 187 | }); | | |
| 188 | | 243 | |
| 189 | if (response.isErr()) { | 244 | if (response.isErr()) { |
| 190 | const cause = response.error.cause; | 245 | const cause = response.error.cause; |
| 191 | const message = cause instanceof Error ? cause.message : String(cause); | 246 | const message = cause instanceof Error ? cause.message : String(cause); |
| 192 | return { ok: false, kind: "failed", error: `Tool callback failed: ${message}` }; | 247 | return { ok: false, kind: "failed", error: `Tool callback failed: ${message}` }; |
| | 248 | } |
| | 249 | |
| | 250 | const result = response.value as ToolCallResult; |
| | 251 | if (!result.ok && result.kind === "pending") { |
| | 252 | if (!result.approvalId) { |
| | 253 | return { ok: false, kind: "failed", error: "Approval pending without approvalId" }; |
| | 254 | } |
| | 255 | |
| | 256 | const approvalId = result.approvalId; |
| | 257 | const wait = await Result.tryPromise(() => this.waitForApprovalUpdate(approvalId)); |
| | 258 | if (wait.isErr()) { |
| | 259 | const cause = wait.error.cause; |
| | 260 | const message = cause instanceof Error ? cause.message : String(cause); |
| | 261 | return { ok: false, kind: "failed", error: `Approval subscription failed: ${message}` }; |
| | 262 | } |
| | 263 | continue; |
| | 264 | } |
| | 265 | |
| | 266 | return result; |
| 193 | } | 267 | } |
| 194 | | | |
| 195 | return response.value as ToolCallResult; | | |
| 196 | } | 268 | } |
| 197 | } | 269 | } |
| 198 | | 270 | |