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
2import { api } from "@executor/database/convex/_generated/api";2import { api } from "@executor/database/convex/_generated/api";
3import { ConvexClient, ConvexHttpClient } from "convex/browser";3import { ConvexClient, ConvexHttpClient } from "convex/browser";
4import type {4import type {
5 BridgeEntrypointContext,
6 BridgeProps,5 BridgeProps,
7 ToolCallResult,6 ToolCallResult,
8 WorkerEntrypointExports,7 WorkerEntrypointExports,
@@ -10,9 +9,15 @@ import type { · 1 unmodified line
10 9
11const APPROVAL_SUBSCRIPTION_TIMEOUT_MS = 10 * 60 * 1000;10const APPROVAL_SUBSCRIPTION_TIMEOUT_MS = 10 * 60 * 1000;
12 11
12function 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
13function isBridgeProps(value: unknown): value is BridgeProps {19function isBridgeProps(value: unknown): value is BridgeProps {
14 if (!value || typeof value !== "object") return false;20 const props = asObject(value);
15 const props = value as Partial<BridgeProps>;
16 return (21 return (
17 typeof props.callbackConvexUrl === "string"22 typeof props.callbackConvexUrl === "string"
18 && typeof props.callbackInternalSecret === "string"23 && typeof props.callbackInternalSecret === "string"
@@ -20,33 +25,46 @@ function isBridgeProps(value: unknown): value is BridgeProps { · 1 unmodified line
20 );25 );
21}26}
22 27
28function 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
23export function getBridgePropsFromContext(ctx: unknown): BridgeProps {41export function getBridgePropsFromContext(ctx: unknown): BridgeProps {
24 if (!ctx || typeof ctx !== "object") {42 const context = asObject(ctx);
43 if (Object.keys(context).length === 0) {
25 throw new Error("WorkerEntrypoint context is unavailable");44 throw new Error("WorkerEntrypoint context is unavailable");
26 }45 }
27 46
28 const maybeContext = ctx as Partial<BridgeEntrypointContext>;47 if (!isBridgeProps(context.props)) {
29 if (!isBridgeProps(maybeContext.props)) {
30 throw new Error("ToolBridge props are missing or invalid");48 throw new Error("ToolBridge props are missing or invalid");
31 }49 }
32 50
33 return maybeContext.props;51 return context.props;
34}52}
35 53
36export function getEntrypointExports(ctx: ExecutionContext): WorkerEntrypointExports {54export function getEntrypointExports(ctx: ExecutionContext): WorkerEntrypointExports {
37 const maybeCtx = ctx as ExecutionContext & { exports?: unknown };55 const context = asObject(ctx);
38 const exportsValue = maybeCtx.exports;56 const exportsValue = context.exports;
39 57
40 if (!exportsValue || typeof exportsValue !== "object") {58 if (!exportsValue || typeof exportsValue !== "object") {
41 throw new Error("Execution context exports are unavailable");59 throw new Error("Execution context exports are unavailable");
42 }60 }
43 61
44 const maybeExports = exportsValue as Partial<WorkerEntrypointExports>;62 const exportsObject = asObject(exportsValue);
45 if (typeof maybeExports.ToolBridge !== "function") {63 if (typeof exportsObject.ToolBridge !== "function") {
46 throw new Error("Execution context ToolBridge export is unavailable");64 throw new Error("Execution context ToolBridge export is unavailable");
47 }65 }
48 66
49 return { ToolBridge: maybeExports.ToolBridge };67 return { ToolBridge: exportsObject.ToolBridge as WorkerEntrypointExports["ToolBridge"] };
50}68}
51 69
52function createConvexClient(callbackConvexUrl: string): ConvexHttpClient {70function createConvexClient(callbackConvexUrl: string): ConvexHttpClient {
@@ -136,7 +154,11 @@ export async function callToolWithBridge( · 83 unmodified lines
136 return { ok: false, kind: "failed", error: `Tool callback failed: ${message}` };154 return { ok: false, kind: "failed", error: `Tool callback failed: ${message}` };
137 }155 }
138 156
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
140 if (!result.ok && result.kind === "pending") {162 if (!result.ok && result.kind === "pending") {
141 if (!result.approvalId) {163 if (!result.approvalId) {
142 return { ok: false, kind: "failed", error: "Approval pending without approvalId" };164 return { ok: false, kind: "failed", error: "Approval pending without approvalId" };