History

Harden bridge context parsing with explicit result guards

Rhys Sullivan committed 8 months ago

Commit 8a4b1691 parent c779d1a1 file · +35 −13Browse files

Unified · Split

executor/packages/runner-sandbox-host/src/bridge.ts+35−13
@@ -2,7 +2,6 @@ import { Result } from "better-result"; · 1 unmodified line
22 import { api } from "@executor/database/convex/_generated/api";
33 import { ConvexClient, ConvexHttpClient } from "convex/browser";
44 import type {
5− BridgeEntrypointContext,
65 BridgeProps,
76 ToolCallResult,
87 WorkerEntrypointExports,
@@ -10,9 +9,15 @@ import type { · 1 unmodified line
109
1110 const APPROVAL_SUBSCRIPTION_TIMEOUT_MS = 10 * 60 * 1000;
1211
12+function asObject(value: unknown): Record<string, unknown> {
13+ if (value && typeof value === "object" && !Array.isArray(value)) {
14+ return value as Record<string, unknown>;
15+ }
16+ return {};
17+}
18+
1319 function isBridgeProps(value: unknown): value is BridgeProps {
14− if (!value || typeof value !== "object") return false;
15− const props = value as Partial<BridgeProps>;
20+ const props = asObject(value);
1621 return (
1722 typeof props.callbackConvexUrl === "string"
1823 && typeof props.callbackInternalSecret === "string"
@@ -20,33 +25,46 @@ function isBridgeProps(value: unknown): value is BridgeProps { · 1 unmodified line
2025 );
2126 }
2227
28+function isToolCallResult(value: unknown): value is ToolCallResult {
29+ const record = asObject(value);
30+ if (record.ok === true) {
31+ return true;
32+ }
33+
34+ if (record.ok === false) {
35+ return record.kind === "pending" || record.kind === "denied" || record.kind === "failed";
36+ }
37+
38+ return false;
39+}
40+
2341 export function getBridgePropsFromContext(ctx: unknown): BridgeProps {
24− if (!ctx || typeof ctx !== "object") {
42+ const context = asObject(ctx);
43+ if (Object.keys(context).length === 0) {
2544 throw new Error("WorkerEntrypoint context is unavailable");
2645 }
2746
28− const maybeContext = ctx as Partial<BridgeEntrypointContext>;
29− if (!isBridgeProps(maybeContext.props)) {
47+ if (!isBridgeProps(context.props)) {
3048 throw new Error("ToolBridge props are missing or invalid");
3149 }
3250
33− return maybeContext.props;
51+ return context.props;
3452 }
3553
3654 export function getEntrypointExports(ctx: ExecutionContext): WorkerEntrypointExports {
37− const maybeCtx = ctx as ExecutionContext & { exports?: unknown };
38− const exportsValue = maybeCtx.exports;
55+ const context = asObject(ctx);
56+ const exportsValue = context.exports;
3957
4058 if (!exportsValue || typeof exportsValue !== "object") {
4159 throw new Error("Execution context exports are unavailable");
4260 }
4361
44− const maybeExports = exportsValue as Partial<WorkerEntrypointExports>;
45− if (typeof maybeExports.ToolBridge !== "function") {
62+ const exportsObject = asObject(exportsValue);
63+ if (typeof exportsObject.ToolBridge !== "function") {
4664 throw new Error("Execution context ToolBridge export is unavailable");
4765 }
4866
49− return { ToolBridge: maybeExports.ToolBridge };
67+ return { ToolBridge: exportsObject.ToolBridge as WorkerEntrypointExports["ToolBridge"] };
5068 }
5169
5270 function createConvexClient(callbackConvexUrl: string): ConvexHttpClient {
@@ -136,7 +154,11 @@ export async function callToolWithBridge( · 83 unmodified lines
136154 return { ok: false, kind: "failed", error: `Tool callback failed: ${message}` };
137155 }
138156
139− const result = response.value as ToolCallResult;
157+ const result = response.value;
158+ if (!isToolCallResult(result)) {
159+ return { ok: false, kind: "failed", error: "Tool callback returned invalid result payload" };
160+ }
161+
140162 if (!result.ok && result.kind === "pending") {
141163 if (!result.approvalId) {
142164 return { ok: false, kind: "failed", error: "Approval pending without approvalId" };