{"version":3,"file":"sandbox-runtime.mjs","names":[],"sources":["../../../src/services/sandbox-runtime.ts"],"sourcesContent":["/**\n * Sandbox Runtime — restricted execution environment for user-defined tools.\n *\n * Provides a controlled execution context that:\n * 1. Enforces budget limits (max USD cost per execution)\n * 2. Restricts network access to allowlisted endpoints only\n * 3. Limits execution time (timeout)\n * 4. Caps the number of sub-tool calls for composed/custom tools\n * 5. Audits all actions for the tool creator's review\n *\n * The sandbox does NOT use V8 isolates or Worker threads — it enforces\n * limits at the application layer via budget tracking, allowlist gating,\n * and call counting. This is sufficient for our threat model (the agent\n * is the executor, not arbitrary user code).\n */\n\nimport { jsonResult, errorResult } from '../lib/tool-helpers.js';\nimport { guardedFetch, addAllowedHost, isAllowedEndpoint } from './endpoint-allowlist.js';\nimport type { UserTool, ApiConnectorDef, ComposedStep } from './user-tool-service.js';\n\n// ─── Types ──────────────────────────────────────────────────────────────\n\nexport interface SandboxContext {\n  /** The user tool being executed. */\n  tool: UserTool;\n  /** Who triggered the execution. */\n  userId: string;\n  /** Budget remaining (USD). Decremented by sub-calls. */\n  budgetRemainingUsd: number;\n  /** Number of sub-tool calls made. */\n  callCount: number;\n  /** Max sub-tool calls allowed. */\n  maxCalls: number;\n  /** Execution start time. */\n  startedAt: number;\n  /** Max execution time (ms). */\n  timeoutMs: number;\n  /** Audit log of actions taken. */\n  auditLog: SandboxAuditEntry[];\n}\n\nexport interface SandboxAuditEntry {\n  timestamp: number;\n  action: string;\n  detail: string;\n  costUsd?: number;\n}\n\nexport interface ToolDispatcher {\n  /** Call a built-in tool by name. Returns the tool result. */\n  call(toolName: string, args: Record<string, unknown>): Promise<any>;\n}\n\n// ─── Sandbox Execution ──────────────────────────────────────────────────\n\n/**\n * Execute an API connector tool in the sandbox.\n */\nexport async function executeApiConnector(\n  def: ApiConnectorDef,\n  args: Record<string, unknown>,\n  ctx: SandboxContext,\n): Promise<any> {\n  // Check budget\n  if (ctx.budgetRemainingUsd <= 0) {\n    return errorResult('Budget exhausted for this tool execution.');\n  }\n\n  // Check timeout\n  if (Date.now() - ctx.startedAt > ctx.timeoutMs) {\n    return errorResult('Tool execution timed out.');\n  }\n\n  // Build URL from template\n  let path = def.path;\n  for (const [key, value] of Object.entries(args)) {\n    path = path.replace(`{{${key}}}`, encodeURIComponent(String(value)));\n  }\n  const url = `${def.baseUrl}${path}`;\n\n  // Validate URL is allowed\n  if (!isAllowedEndpoint(url)) {\n    // For user tools, we dynamically allow the configured baseUrl\n    try {\n      const parsed = new URL(def.baseUrl);\n      addAllowedHost(parsed.hostname);\n    } catch {\n      return errorResult(`Cannot reach \"${url}\" — not in allowlist and baseUrl is invalid.`);\n    }\n  }\n\n  // Build headers, resolving secret references\n  const headers: Record<string, string> = { 'Accept': 'application/json' };\n  if (def.headers) {\n    for (const [key, value] of Object.entries(def.headers)) {\n      if (value.startsWith('$SECRET:')) {\n        // Secret references are resolved by the credential vault at a higher layer\n        // For now, pass through as-is (the caller should have resolved them)\n        headers[key] = value;\n      } else {\n        headers[key] = value;\n      }\n    }\n  }\n\n  // Build body\n  let body: string | undefined;\n  if (def.bodyTemplate && ['POST', 'PUT', 'PATCH'].includes(def.method)) {\n    body = def.bodyTemplate;\n    for (const [key, value] of Object.entries(args)) {\n      body = body.replace(`{{${key}}}`, JSON.stringify(value).replace(/^\"|\"$/g, ''));\n    }\n    headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';\n  }\n\n  // Audit\n  ctx.auditLog.push({\n    timestamp: Date.now(),\n    action: 'http_request',\n    detail: `${def.method} ${url}`,\n  });\n  ctx.callCount += 1;\n\n  // Execute\n  try {\n    const response = await guardedFetch(url, {\n      method: def.method,\n      headers,\n      body,\n      signal: AbortSignal.timeout(def.timeoutMs ?? 15_000),\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => '');\n      return errorResult(`API returned ${response.status}: ${text.slice(0, 200)}`);\n    }\n\n    const data: any = await response.json();\n\n    // Extract result using resultPath\n    let result = data;\n    if (def.resultPath) {\n      for (const key of def.resultPath.split('.')) {\n        result = result?.[key];\n      }\n    }\n\n    return jsonResult(result ?? data);\n  } catch (err) {\n    return errorResult(`API call failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\n/**\n * Execute a composed tool in the sandbox.\n */\nexport async function executeComposedTool(\n  steps: ComposedStep[],\n  args: Record<string, unknown>,\n  ctx: SandboxContext,\n  dispatcher: ToolDispatcher,\n): Promise<any> {\n  const results: any[] = [];\n\n  for (let i = 0; i < steps.length; i++) {\n    const step = steps[i]!;\n\n    // Check limits\n    if (ctx.budgetRemainingUsd <= 0) {\n      return errorResult(`Budget exhausted at step ${i + 1} (\"${step.label}\").`);\n    }\n    if (Date.now() - ctx.startedAt > ctx.timeoutMs) {\n      return errorResult(`Timeout at step ${i + 1} (\"${step.label}\").`);\n    }\n    if (ctx.callCount >= ctx.maxCalls) {\n      return errorResult(`Max tool calls reached at step ${i + 1} (\"${step.label}\").`);\n    }\n\n    // Resolve step args — replace \"$step.N.field\" references\n    const resolvedArgs: Record<string, unknown> = {};\n    for (const [key, value] of Object.entries(step.args)) {\n      if (typeof value === 'string' && value.startsWith('$step.')) {\n        const match = value.match(/^\\$step\\.(\\d+)\\.(.+)$/);\n        if (match) {\n          const stepIdx = parseInt(match[1]!, 10);\n          const field = match[2]!;\n          const prevResult = results[stepIdx];\n          resolvedArgs[key] = prevResult?.[field] ?? value;\n        } else {\n          resolvedArgs[key] = value;\n        }\n      } else if (typeof value === 'string' && value.startsWith('$arg.')) {\n        const argName = value.slice(5);\n        resolvedArgs[key] = args[argName] ?? value;\n      } else {\n        resolvedArgs[key] = value;\n      }\n    }\n\n    // Audit\n    ctx.auditLog.push({\n      timestamp: Date.now(),\n      action: 'tool_call',\n      detail: `Step ${i + 1}: ${step.tool}(${JSON.stringify(resolvedArgs).slice(0, 100)})`,\n    });\n    ctx.callCount += 1;\n\n    // Execute step\n    try {\n      const result = await dispatcher.call(step.tool, resolvedArgs);\n      // Parse result if it's a tool result shape\n      let parsed = result;\n      if (result?.content?.[0]?.text) {\n        try { parsed = JSON.parse(result.content[0].text); } catch { parsed = result.content[0].text; }\n      }\n      results.push(parsed);\n\n      if (result?.isError && (step.stopOnFailure !== false)) {\n        return errorResult(`Step ${i + 1} (\"${step.label}\") failed: ${result.content?.[0]?.text ?? 'unknown error'}`);\n      }\n    } catch (err) {\n      if (step.stopOnFailure !== false) {\n        return errorResult(`Step ${i + 1} (\"${step.label}\") threw: ${err instanceof Error ? err.message : String(err)}`);\n      }\n      results.push({ error: err instanceof Error ? err.message : String(err) });\n    }\n  }\n\n  return jsonResult({\n    stepsCompleted: results.length,\n    totalSteps: steps.length,\n    results,\n    lastResult: results[results.length - 1],\n  });\n}\n\n/**\n * Create a sandbox context for tool execution.\n */\nexport function createSandboxContext(tool: UserTool, userId: string): SandboxContext {\n  const maxCalls = tool.definition.type === 'custom'\n    ? (tool.definition as any).maxCalls ?? 5\n    : tool.definition.type === 'composed'\n      ? (tool.definition as any).steps?.length ?? 10\n      : 1;\n\n  return {\n    tool,\n    userId,\n    budgetRemainingUsd: tool.maxBudgetUsd,\n    callCount: 0,\n    maxCalls,\n    startedAt: Date.now(),\n    timeoutMs: 30_000,\n    auditLog: [],\n  };\n}\n"],"mappings":"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"}