> ## Documentation Index
> Fetch the complete documentation index at: https://buildfunctions.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Runtime Controls - Docs

> Composable guardrails for agent tool calls

Runtime Controls wrap your functions with composable guardrails so agents can run autonomously without spiraling into infinite loops, duplicate writes, or runaway tool-call volume. No API key required — they work standalone around your own code, or combined with Buildfunctions hardware-isolated sandboxes.

<Note>Runtime Controls is in beta. The SDK interface may change between releases.</Note>

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install buildfunctions
  ```

  ```bash yarn theme={null}
  yarn add buildfunctions
  ```

  ```bash pnpm theme={null}
  pnpm add buildfunctions
  ```

  ```bash pip theme={null}
  pip install buildfunctions
  ```
</CodeGroup>

## Why Runtime Controls

When agents run unattended, things go wrong:

* **Infinite loops** - the agent retries the same failing call forever
* **Runaway tool-call volume** - unchecked tool calls can spike API usage quickly
* **Duplicate side effects** - the same write runs twice
* **Unsafe actions** - an agent executes destructive operations
* **Cascading failures** - one broken dependency can fan out failures

Runtime Controls reduce these failure modes, but you still need to configure the right controls for your workflow. `maxToolCalls` is a call-count guardrail, not direct provider billing telemetry.

## Examples

### Wrap Any Async Call (No API Key)

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  import { RuntimeControls } from 'buildfunctions'

  const controls = RuntimeControls.create({
    maxToolCalls: 50,
    timeoutMs: 30_000,
    retry: { maxAttempts: 3, initialDelayMs: 200, backoffFactor: 2 },
    loopBreaker: { warningThreshold: 5, quarantineThreshold: 8, stopThreshold: 12 },
    onEvent: (event) => console.log(`[controls] ${event.type}: ${event.message}`),
  })

  const guardedFetch = controls.wrap({
    toolName: 'api-call',
    runKey: 'agent-run-1',
    destination: 'https://api.example.com',
    run: async ([payload]) => {
      const res = await fetch('https://api.example.com/data', {
        method: 'POST',
        body: JSON.stringify(payload),
      })
      return res.json()
    },
  })

  const result = await guardedFetch({ query: 'latest results' })
  ```

  ```python Python theme={null}
  import httpx
  from buildfunctions import RuntimeControls

  controls = RuntimeControls.create({
      "maxToolCalls": 50,
      "timeoutMs": 30_000,
      "retry": {"maxAttempts": 3, "initialDelayMs": 200, "backoffFactor": 2},
      "loopBreaker": {"warningThreshold": 5, "quarantineThreshold": 8, "stopThreshold": 12},
      "onEvent": lambda event: print(f"[controls] {event['type']}: {event['message']}"),
  })

  async def run_api(args, runtime):
      _ = runtime
      payload = args[0]
      async with httpx.AsyncClient() as client:
          response = await client.post(
              "https://api.example.com/data",
              json=payload,
              timeout=30,
          )
          response.raise_for_status()
          return response.json()

  wrapped_fetch = controls.wrap({
      "toolName": "api-call",
      "runKey": "agent-run-1",
      "destination": "https://api.example.com",
      "run": run_api,
  })

  result = await wrapped_fetch({"query": "latest results"})
  ```
</CodeGroup>

### Use `run()` Directly

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  import { RuntimeControls } from 'buildfunctions'

  const controls = RuntimeControls.create({ retry: { maxAttempts: 2 } })

  const result = await controls.run(
    { toolName: 'simple-tool' },
    async () => 'ok'
  )
  ```

  ```python Python theme={null}
  from buildfunctions import RuntimeControls

  controls = RuntimeControls.create({"retry": {"maxAttempts": 2}})

  async def execute(runtime):
      return "ok"

  result = await controls.run({"toolName": "simple-tool"}, execute)
  ```
</CodeGroup>

`runtime["signal"]` is available in handlers but optional. Use it when your tool call supports cancellation.

## Control Layers

Every control is opt-in and composable. Enable only what you need.

### Retry with Exponential Backoff

Automatically retries transient failures (`503`, `429`, timeouts) with configurable backoff.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    retry: {
      maxAttempts: 4,
      initialDelayMs: 250,
      maxDelayMs: 10_000,
      backoffFactor: 2,
      jitterRatio: 0.2,
    },
  })
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({
      "retry": {
          "maxAttempts": 4,
          "initialDelayMs": 250,
          "maxDelayMs": 10_000,
          "backoffFactor": 2,
          "jitterRatio": 0.2,
      }
  })
  ```
</CodeGroup>

Custom retry decisions:

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    retry: { maxAttempts: 4 },
    retryClassifier: ({ error, statusCode }) => {
      if (statusCode === 409) {
        return { retryable: true, delayMs: 500, reason: 'conflict_backoff' }
      }
      return { retryable: error.code === 'NETWORK_ERROR' }
    },
  })
  ```

  ```python Python theme={null}
  def retry_classifier(ctx):
      if ctx.get("statusCode") == 409:
          return {"retryable": True, "delayMs": 500, "reason": "conflict_backoff"}
      return {"retryable": ctx["error"]["code"] == "NETWORK_ERROR"}

  controls = RuntimeControls.create({
      "retry": {"maxAttempts": 4},
      "retryClassifier": retry_classifier,
  })
  ```
</CodeGroup>

### Tool-Call Budget

Per-run cap on the number of tool calls when `maxToolCalls` is configured. Budgets are scoped by `runKey`.
This is a tool-call-count budget, not a dollar-based cost meter.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    maxToolCalls: 50,
  })

  await controls.run({ toolName: 'shell', runKey: 'agent-run-1' }, async () => 'ok')

  // Reset budget when starting a new run.
  await controls.reset('agent-run-1')
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({"maxToolCalls": 50})

  async def execute_step(runtime):
      _ = runtime
      return await do_work()

  await controls.run({"toolName": "shell", "runKey": "agent-run-1"}, execute_step)
  await controls.reset("agent-run-1")
  ```
</CodeGroup>

### Loop Breaker

Detects repeated no-progress patterns and escalates from warning to quarantine to stop.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    loopBreaker: {
      enabled: true,
      warningThreshold: 5,
      quarantineThreshold: 8,
      stopThreshold: 12,
      quarantineMs: 15_000,
      stopCooldownMs: 120_000,
    },
  })
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({
      "loopBreaker": {
          "enabled": True,
          "warningThreshold": 5,
          "quarantineThreshold": 8,
          "stopThreshold": 12,
          "quarantineMs": 15_000,
          "stopCooldownMs": 120_000,
      }
  })
  ```
</CodeGroup>

### Circuit Breaker

Temporarily blocks calls to a failing dependency. Keyed by `(tenant, toolName, destination)`.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    circuitBreaker: {
      enabled: true,
      windowMs: 30_000,
      minRequests: 20,
      failureRateThreshold: 0.6,
      cooldownMs: 60_000,
    },
  })
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({
      "circuitBreaker": {
          "enabled": True,
          "windowMs": 30_000,
          "minRequests": 20,
          "failureRateThreshold": 0.6,
          "cooldownMs": 60_000,
      }
  })
  ```
</CodeGroup>

When failure rate exceeds threshold inside the sliding window, the circuit opens for `cooldownMs`.

### Timeout and Cancellation

Per-call timeout with abort signal propagation.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({ timeoutMs: 30_000 })

  const controller = new AbortController()
  await controls.run(
    { toolName: 'shell', signal: controller.signal },
    async ({ signal }) => {
      return runCommand('npm test', { signal })
    }
  )
  ```

  ```python Python theme={null}
  from buildfunctions import RuntimeControls, create_abort_controller

  controls = RuntimeControls.create({"timeoutMs": 30_000})
  controller = create_abort_controller()

  async def execute(runtime):
      signal = runtime["signal"]
      return await run_command("npm test", signal=signal)

  task = controls.run({"toolName": "shell", "signal": controller.signal}, execute)
  # later if needed:
  # controller.abort(Exception("cancelled"))
  result = await task
  ```
</CodeGroup>

### Policy Gates

Deterministic `allow` / `deny` / `require_approval` rules with specificity-based precedence.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    policy: {
      mode: 'enforce',
      rules: [
        {
          id: 'deny-destructive',
          action: 'deny',
          tools: ['repo-admin'],
          actionPrefixes: ['delete'],
          reason: 'Destructive repo operations are blocked',
        },
        {
          id: 'approve-external-writes',
          action: 'require_approval',
          tools: ['ticket-write'],
          destinations: ['*.external.example.com'],
          reason: 'External writes require human approval',
        },
      ],
      approvalHandler: async () => false,
    },
  })
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({
      "policy": {
          "mode": "enforce",  # or "dryRun"
          "rules": [
              {
                  "id": "deny-destructive",
                  "action": "deny",
                  "tools": ["repo-admin"],
                  "actionPrefixes": ["delete"],
                  "reason": "Destructive repo operations are blocked",
              },
              {
                  "id": "approve-external-writes",
                  "action": "require_approval",
                  "tools": ["ticket-write"],
                  "destinations": ["*.external.example.com"],
                  "reason": "External writes require human approval",
              },
          ],
          "approvalHandler": lambda ctx: False,
      }
  })
  ```
</CodeGroup>

**Policy precedence** (highest wins):

1. Higher tool specificity (exact name > prefix > wildcard)
2. Higher destination specificity (exact host > wildcard subdomain > `*`)
3. Longer action prefix match
4. Stricter action (`deny` > `require_approval` > `allow`)
5. Earlier rule index on exact tie (current behavior)

### Idempotency

Prevents duplicate side effects by replaying prior results for calls with the same `idempotencyKey`.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    idempotency: {
      enabled: true,
      ttlMs: 600_000,
      includeErrors: false,
      namespaceByRunKey: true,
    },
  })

  await controls.run(
    {
      toolName: 'pr-comment',
      runKey: 'pr-4821',
      idempotencyKey: 'comment:pr-4821:summary',
    },
    async () => postComment('LGTM')
  )

  await controls.run(
    {
      toolName: 'pr-comment',
      runKey: 'pr-4821',
      idempotencyKey: 'comment:pr-4821:summary',
    },
    async () => postComment('LGTM') // replayed, does not execute again
  )
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({
      "idempotency": {
          "enabled": True,
          "ttlMs": 600_000,
          "includeErrors": False,
          "namespaceByRunKey": True,
      }
  })

  context = {
      "toolName": "pr-comment",
      "runKey": "pr-4821",
      "idempotencyKey": "comment:pr-4821:summary",
  }

  async def post_summary(runtime):
      _ = runtime
      return await post_comment("LGTM")

  await controls.run(context, post_summary)
  await controls.run(context, post_summary)  # replayed, does not execute again
  ```
</CodeGroup>

### Concurrency Locks

Prevents simultaneous conflicting writes to the same resource.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    concurrency: {
      enabled: true,
      waitMode: 'reject', // or 'wait'
      leaseMs: 30_000,
      waitTimeoutMs: 5_000,
    },
  })

  await controls.run(
    {
      toolName: 'repo-write',
      resourceKey: 'repo:buildfunctions/web-app',
    },
    async () => gitPush('main')
  )
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({
      "concurrency": {
          "enabled": True,
          "waitMode": "reject",  # or "wait"
          "leaseMs": 30_000,
          "waitTimeoutMs": 5_000,
          "pollIntervalMs": 50,
      }
  })

  async def push_main(runtime):
      _ = runtime
      return await git_push("main")

  await controls.run(
      {"toolName": "repo-write", "resourceKey": "repo:buildfunctions/web-app"},
      push_main,
  )
  ```
</CodeGroup>

### Verifier Hooks

Custom correctness gates that run before execution, after success, and after errors.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    verifiers: {
      beforeCall: ({ toolName, action }) => {
        if (toolName === 'repo-admin' && action?.startsWith('delete')) {
          return { allow: false, reason: 'delete blocked' }
        }
        return { allow: true }
      },
      afterSuccess: ({ result }) => {
        if (!result || typeof result !== 'object') {
          return { allow: false, reason: 'unexpected result shape' }
        }
        return { allow: true }
      },
      afterError: ({ error }) => {
        return { allow: true }
      },
    },
  })
  ```

  ```python Python theme={null}
  def before_call(ctx):
      if ctx["toolName"] == "repo-admin" and str(ctx.get("action", "")).startswith("delete"):
          return {"allow": False, "reason": "delete blocked"}
      return {"allow": True}

  def after_success(ctx):
      result = ctx.get("result")
      if not isinstance(result, dict):
          return {"allow": False, "reason": "unexpected result shape"}
      return {"allow": True}

  controls = RuntimeControls.create({
      "verifiers": {
          "beforeCall": before_call,
          "afterSuccess": after_success,
      }
  })
  ```
</CodeGroup>

### Per-Tool and Per-Destination Overrides

Apply stricter or looser controls to specific tools or destinations.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    timeoutMs: 60_000,
    retry: { maxAttempts: 4 },
    overrides: {
      tools: {
        shell: { timeoutMs: 10_000, retry: { maxAttempts: 2 } },
      },
      destinations: {
        '*.internal-api.example.com': { timeoutMs: 5_000 },
      },
    },
  })
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({
      "timeoutMs": 60_000,
      "retry": {"maxAttempts": 4},
      "overrides": {
          "tools": {
              "shell": {"timeoutMs": 10_000, "retry": {"maxAttempts": 2}},
          },
          "destinations": {
              "*.internal-api.example.com": {"timeoutMs": 5_000},
          },
      },
  })
  ```
</CodeGroup>

Tool overrides win when both tool and destination set the same field.

## Agent Logic Safety

`applyAgentLogicSafety` adds pre-execution safety checks without changing RuntimeControls internals.

### Injection Guard

Scans tool name, action, destination, and args for injection patterns before execution.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  import { RuntimeControls, applyAgentLogicSafety } from 'buildfunctions'

  const controls = RuntimeControls.create(
    applyAgentLogicSafety(
      { retry: { maxAttempts: 2 } },
      {
        injectionGuard: {
          enabled: true,
          patterns: [
            /ignore\s+previous\s+instructions/i,
            /\brm\s+-rf\b/i,
            /<script\b/i,
          ],
        },
      }
    )
  )
  ```

  ```python Python theme={null}
  import re
  from buildfunctions import RuntimeControls, applyAgentLogicSafety

  controls = RuntimeControls.create(
      applyAgentLogicSafety(
          {"retry": {"maxAttempts": 2}},
          {
              "injectionGuard": {
                  "enabled": True,
                  "patterns": [
                      re.compile(r"ignore\s+previous\s+instructions", re.I),
                      re.compile(r"\brm\s+-rf\b", re.I),
                      re.compile(r"<script\b", re.I),
                  ],
              },
          },
      )
  )
  ```
</CodeGroup>

Default patterns (when `patterns` is omitted):

* `ignore (all|any|previous) instructions`
* `system prompt`
* `developer message`
* `<script`
* `rm -rf`

### Exit Conditions

Enforces maximum steps per run and can block calls after a terminal action.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create(
    applyAgentLogicSafety(
      {},
      {
        exitCondition: {
          enabled: true,
          maxStepsPerRun: 30,
          terminalActions: [{ toolNamePattern: 'agent-control', actionPrefix: 'finish' }],
          blockAfterTerminal: true,
        },
      }
    )
  )
  ```

  ```python Python theme={null}
  from buildfunctions import RuntimeControls, applyAgentLogicSafety

  controls = RuntimeControls.create(
      applyAgentLogicSafety(
          {},
          {
              "exitCondition": {
                  "enabled": True,
                  "maxStepsPerRun": 30,
                  "terminalActions": [{"toolNamePattern": "agent-control", "actionPrefix": "finish"}],
                  "blockAfterTerminal": True,
              },
          },
      )
  )
  ```
</CodeGroup>

### Intent Allowlist

Restricts which tool + action combinations are permitted. Anything not explicitly allowed is denied.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create(
    applyAgentLogicSafety(
      {},
      {
        intentAllowlist: {
          enabled: true,
          rules: [
            { toolNamePattern: 'cpu-sandbox', actionPrefixes: ['run_'] },
            { toolNamePattern: 'repo-write', actionPrefixes: ['push_'] },
            { toolNamePattern: 'pr-comment', actionPrefixes: ['post_'] },
          ],
        },
      }
    )
  )
  ```

  ```python Python theme={null}
  from buildfunctions import RuntimeControls, applyAgentLogicSafety

  controls = RuntimeControls.create(
      applyAgentLogicSafety(
          {},
          {
              "intentAllowlist": {
                  "enabled": True,
                  "rules": [
                      {"toolNamePattern": "cpu-sandbox", "actionPrefixes": ["run_"]},
                      {"toolNamePattern": "repo-write", "actionPrefixes": ["push_"]},
                      {"toolNamePattern": "pr-comment", "actionPrefixes": ["post_"]},
                  ],
              },
          },
      )
  )
  ```
</CodeGroup>

### Combining All Three

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create(
    applyAgentLogicSafety(
      {
        tenantKey: 'my-team-prod',
        maxToolCalls: 100,
        retry: { maxAttempts: 3 },
        concurrency: { enabled: true, waitMode: 'reject', leaseMs: 30_000 },
      },
      {
        injectionGuard: { enabled: true },
        exitCondition: {
          enabled: true,
          maxStepsPerRun: 50,
          terminalActions: [{ toolNamePattern: 'agent-control', actionPrefix: 'finish' }],
          blockAfterTerminal: true,
        },
        intentAllowlist: {
          enabled: true,
          rules: [
            { toolNamePattern: 'cpu-sandbox', actionPrefixes: ['run_'] },
            { toolNamePattern: 'repo-write', actionPrefixes: ['push_'] },
            { toolNamePattern: 'pr-comment', actionPrefixes: ['post_'] },
          ],
        },
      }
    )
  )
  ```

  ```python Python theme={null}
  import re
  from buildfunctions import RuntimeControls, applyAgentLogicSafety

  controls = RuntimeControls.create(
      applyAgentLogicSafety(
          {
              "tenantKey": "my-team-prod",
              "maxToolCalls": 100,
              "retry": {"maxAttempts": 3},
              "concurrency": {"enabled": True, "waitMode": "reject", "leaseMs": 30_000},
          },
          {
              "injectionGuard": {
                  "enabled": True,
                  "patterns": [
                      re.compile(r"ignore\s+previous\s+instructions", re.I),
                      re.compile(r"\brm\s+-rf\b", re.I),
                  ],
              },
              "exitCondition": {
                  "enabled": True,
                  "maxStepsPerRun": 50,
                  "terminalActions": [{"toolNamePattern": "agent-control", "actionPrefix": "finish"}],
                  "blockAfterTerminal": True,
              },
              "intentAllowlist": {
                  "enabled": True,
                  "rules": [
                      {"toolNamePattern": "cpu-sandbox", "actionPrefixes": ["run_"]},
                      {"toolNamePattern": "repo-write", "actionPrefixes": ["push_"]},
                      {"toolNamePattern": "pr-comment", "actionPrefixes": ["post_"]},
                  ],
              },
          },
      )
  )
  ```
</CodeGroup>

## Observability

### Event Callback

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    onEvent: (event) => {
      console.log(`[${event.type}] ${event.message}`, event.details)
    },
  })
  ```

  ```python Python theme={null}
  controls = RuntimeControls.create({
      "onEvent": lambda event: print(f"[{event['type']}] {event['message']} {event.get('details')}")
  })
  ```
</CodeGroup>

### Event Sinks

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const controls = RuntimeControls.create({
    eventSinks: [
      async (event) => await writeToDatabase(event),
      async (event) => await sendToMonitoring(event),
    ],
    onEventSinkFailure: ({ failure, sinkIndex }) => {
      console.error(`Event sink ${sinkIndex} failed:`, failure)
    },
  })
  ```

  ```python Python theme={null}
  async def sink_a(event):
      await write_to_database(event)

  async def sink_b(event):
      await send_to_monitoring(event)

  controls = RuntimeControls.create({
      "eventSinks": [sink_a, sink_b],
      "onEventSinkFailure": lambda params: print(f"sink {params['sinkIndex']} failed: {params['failure']}"),
  })
  ```
</CodeGroup>

### Event Types

| Event                      | Meaning                                   |
| -------------------------- | ----------------------------------------- |
| `retry`                    | A retry attempt was scheduled             |
| `loop_warning`             | Repeated no-progress pattern detected     |
| `loop_quarantine`          | Pattern quarantined (temporarily blocked) |
| `loop_stop`                | Pattern hard-stopped                      |
| `circuit_open`             | Dependency circuit breaker opened         |
| `budget_stop`              | Run tool-call budget exceeded             |
| `policy_denied`            | Policy denied a call                      |
| `policy_approval_required` | Approval required by policy               |
| `policy_approved`          | Approval granted by handler               |
| `policy_dry_run`           | Simulated policy decision (dry-run mode)  |
| `verifier_rejected`        | Verifier rejected call, result, or error  |
| `idempotency_replay`       | Prior result replayed                     |
| `concurrency_wait`         | Waiting for resource lock                 |
| `concurrency_rejected`     | Lock rejected or wait timed out           |

## State Adapters

By default, state is in-memory. To persist guardrail state across processes, provide adapters.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const stateAdapter = {
    get: async (key) => await store.get(key),
    set: async (key, value) => await store.set(key, value),
    delete: async (key) => await store.delete(key),
    keys: async (prefix) => await store.keys(prefix),
  }

  const controls = RuntimeControls.create({
    state: {
      budget: stateAdapter,
      circuit: stateAdapter,
      loop: stateAdapter,
      lock: stateAdapter,
      idempotency: stateAdapter,
    },
  })
  ```

  ```python Python theme={null}
  async def adapter_get(key: str):
      return await store.get(key)

  async def adapter_set(key: str, value):
      await store.set(key, value)

  async def adapter_delete(key: str):
      await store.delete(key)

  async def adapter_keys(prefix: str):
      return await store.keys(prefix)

  adapter = {
      "get": adapter_get,
      "set": adapter_set,
      "delete": adapter_delete,
      "keys": adapter_keys,
  }

  controls = RuntimeControls.create({
      "state": {
          "budget": adapter,
          "circuit": adapter,
          "loop": adapter,
          "lock": adapter,
          "idempotency": adapter,
      }
  })
  ```
</CodeGroup>

State is namespaced by `tenantKey`, so teams can scope keys when sharing adapter backends. Unit tests cover adapter contracts; validate your backend and lock semantics with integration tests before production use.

## API Methods

### `RuntimeControls.create(config?)`

Creates a new controls instance. All config is optional.

### `controls.run(context, fn)`

Runs `fn(runtime)` through enabled control layers.

Context fields:

* `toolName` (required)
* `runKey`, `destination`, `action`
* `args`
* `idempotencyKey`, `resourceKey`
* `timeoutMs` (per-call override)
* `signal` (caller cancellation)

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const result = await controls.run(
    {
      toolName: 'shell',
      runKey: 'agent-run-42',
      destination: 'sandbox.example.com',
      action: 'run_tests',
      args: { command: 'npm test' },
      idempotencyKey: 'test:42:v1',
      resourceKey: 'workspace:/repo',
      signal: controller.signal,
      timeoutMs: 10_000,
    },
    async ({ signal }) => {
      return runShell('npm test', { signal })
    }
  )
  ```

  ```python Python theme={null}
  async def execute(runtime):
      signal = runtime["signal"]
      return await run_shell("npm test", signal=signal)

  result = await controls.run(
      {
          "toolName": "shell",
          "runKey": "agent-run-42",
          "destination": "sandbox.example.com",
          "action": "run_tests",
          "args": {"command": "npm test"},
          "idempotencyKey": "test:42:v1",
          "resourceKey": "workspace:/repo",
          "signal": controller.signal,
          "timeoutMs": 10_000,
      },
      execute,
  )
  ```
</CodeGroup>

### `controls.wrap(params)`

Creates a reusable guarded function for repeated tool calls.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const guardedShell = controls.wrap({
    toolName: 'shell',
    runKey: 'agent-run-42',
    destination: 'sandbox.example.com',
    run: async ([command], { signal }) => {
      return runShell(command, { signal })
    },
  })

  await guardedShell('npm test')
  await guardedShell('npm run lint')
  ```

  ```python Python theme={null}
  async def run_shell_guarded(args, runtime):
      command = args[0]
      signal = runtime["signal"]
      return await run_shell(command, signal=signal)

  guarded_shell = controls.wrap({
      "toolName": "shell",
      "runKey": "agent-run-42",
      "destination": "sandbox.example.com",
      "run": run_shell_guarded,
  })

  await guarded_shell("npm test")
  await guarded_shell("npm run lint")
  ```
</CodeGroup>

Dynamic context via resolver functions:

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  const guardedShell = controls.wrap({
    toolName: 'shell',
    run: async ([command], { signal }) => runShell(command, { signal }),
    resolveRunKey: (command) => `run-${command}`,
    resolveDestination: () => 'sandbox.example.com',
    resolveAction: (command) => (command.startsWith('rm') ? 'delete' : 'execute'),
    resolveIdempotencyKey: (command) => `shell:${command}`,
    resolveResourceKey: () => 'workspace:/repo',
  })
  ```

  ```python Python theme={null}
  async def run_shell_dynamic(args, runtime):
      command = args[0]
      signal = runtime["signal"]
      return await run_shell(command, signal=signal)

  guarded_shell = controls.wrap({
      "toolName": "shell",
      "run": run_shell_dynamic,
      "resolveRunKey": lambda command: f"run-{command}",
      "resolveDestination": lambda command: "sandbox.example.com",
      "resolveAction": lambda command: "delete" if str(command).startswith("rm") else "execute",
      "resolveIdempotencyKey": lambda command: f"shell:{command}",
      "resolveResourceKey": lambda command: "workspace:/repo",
  })
  ```
</CodeGroup>

### `controls.reset(runKey=None)`

Resets budget counters for the selected run key.

<CodeGroup dropdown>
  ```javascript JavaScript theme={null}
  await controls.reset('agent-run-42')
  ```

  ```python Python theme={null}
  await controls.reset("agent-run-42")
  ```
</CodeGroup>

## Full Configuration Reference

| Config                                | Default     | Description                                                         |
| ------------------------------------- | ----------- | ------------------------------------------------------------------- |
| `tenantKey`                           | `"default"` | Namespace for all state keys                                        |
| `timeoutMs`                           | `60000`     | Per-call timeout (`0` disables)                                     |
| `maxToolCalls`                        | unset       | Per-`runKey` call budget                                            |
| `retry.maxAttempts`                   | `4`         | Total attempts including first                                      |
| `retry.initialDelayMs`                | `250`       | Backoff base delay                                                  |
| `retry.maxDelayMs`                    | `10000`     | Backoff cap                                                         |
| `retry.backoffFactor`                 | `2`         | Exponential multiplier                                              |
| `retry.jitterRatio`                   | `0.2`       | Delay jitter (`0..1`)                                               |
| `retryClassifier`                     | unset       | Custom retry decision callback                                      |
| `loopBreaker.enabled`                 | `true`      | Enables repeated-pattern detection                                  |
| `loopBreaker.warningThreshold`        | `5`         | Emits `loop_warning`                                                |
| `loopBreaker.quarantineThreshold`     | `8`         | Emits `loop_quarantine`, blocks for `quarantineMs`                  |
| `loopBreaker.stopThreshold`           | `12`        | Emits `loop_stop`, blocks for `stopCooldownMs`                      |
| `loopBreaker.quarantineMs`            | `15000`     | Quarantine duration                                                 |
| `loopBreaker.stopCooldownMs`          | `120000`    | Stop duration                                                       |
| `loopBreaker.maxFingerprints`         | `200`       | Max fingerprints retained                                           |
| `circuitBreaker.enabled`              | `true`      | Enables dependency circuit protection                               |
| `circuitBreaker.windowMs`             | `30000`     | Sliding failure window                                              |
| `circuitBreaker.minRequests`          | `20`        | Requests before open decision                                       |
| `circuitBreaker.failureRateThreshold` | `0.6`       | Open when failure rate exceeds this                                 |
| `circuitBreaker.cooldownMs`           | `60000`     | Open duration                                                       |
| `policy.enabled`                      | `true`      | Enables rule enforcement                                            |
| `policy.mode`                         | `"enforce"` | `"dryRun"` emits simulation events only                             |
| `policy.rules`                        | `[]`        | Array of allow/deny/require\_approval rules                         |
| `policy.approvalHandler`              | unset       | Required for `require_approval` rules                               |
| `verifiers.beforeCall`                | unset       | Pre-execution verifier                                              |
| `verifiers.afterSuccess`              | unset       | Post-success verifier                                               |
| `verifiers.afterError`                | unset       | Post-error verifier                                                 |
| `idempotency.enabled`                 | `true`      | Enables replay by `idempotencyKey`                                  |
| `idempotency.ttlMs`                   | unset       | Replay record expiration                                            |
| `idempotency.includeErrors`           | `false`     | Replay final errors when enabled                                    |
| `idempotency.namespaceByRunKey`       | `true`      | Scope replay by run key                                             |
| `concurrency.enabled`                 | `false`     | Enables locking by `resourceKey`                                    |
| `concurrency.leaseMs`                 | `30000`     | Lock lease duration                                                 |
| `concurrency.waitMode`                | `"reject"`  | `"reject"` immediately or `"wait"`                                  |
| `concurrency.waitTimeoutMs`           | `5000`      | Wait timeout                                                        |
| `concurrency.pollIntervalMs`          | `50`        | Poll interval in wait mode                                          |
| `overrides.tools`                     | unset       | Per-tool config overrides                                           |
| `overrides.destinations`              | unset       | Per-destination config overrides                                    |
| `state.*`                             | in-memory   | State adapters (`budget`, `circuit`, `loop`, `lock`, `idempotency`) |
| `onEvent`                             | unset       | Synchronous event callback                                          |
| `eventSinks`                          | `[]`        | Async event fan-out sinks                                           |
| `onEventSinkFailure`                  | unset       | Sink failure callback                                               |

## FAQ

### Do I need a Buildfunctions API token?

No. Runtime Controls works standalone without any API token.

### What should I wrap first?

Start with side-effecting calls:

1. Repository writes (git push, branch operations)
2. External ticket or issue creation
3. Sandbox and test execution
4. PR or issue comments

### What should be denied by default?

Start by denying destructive actions (`delete*`) and requiring approval for external writes.

### Can I use this without agents?

Yes. Runtime Controls wrap functions — API calls, database queries, file operations, shell commands. Validate behavior in your own integration paths.
