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

# Vercel

> Guardrails and durable execution for a coding agent working in a Vercel Sandbox

[Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) runs code in isolated
cloud sandboxes. A coding agent's tools work in a sandbox checkout while its
model loop runs in the Rebuno agent. Each tool call passes through
[policy](/policy) and becomes a recorded step.

The workspace is a [resource](/sdk/python/resources): later dispatches reopen
it, and forks create separate sandboxes from its checkpoints. It stops while a
call waits for approval.

## Register the workspace

The handler registers a sandbox with a checkpoint policy:

```python theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
from rebuno import CheckpointPolicy, resource
from workspace_resource import VercelResource

workspace = await resource(
    "workspace",
    driver=VercelResource(REPO, token),
    checkpoints=CheckpointPolicy(every_steps=5),
)
```

The example's [`VercelResource`](https://github.com/rebuno/rebuno/blob/main/examples/integrations/sandbox/vercel/workspace_resource.py)
implements three operations:

* `create(checkpoint_ref=None)` creates a sandbox from the selected snapshot
  or a fresh checkout, and configures its Git branch.
* `open(binding)` resumes the sandbox name recorded for this execution.
* `checkpoint(handle)` creates a
  [snapshot](https://vercel.com/docs/vercel-sandbox/concepts/snapshots) and
  resumes the sandbox, since taking a snapshot stops its session.

The checkpoint policy is optional. Without it, dispatches and session turns
still reopen the same sandbox, but forks start from a fresh checkout. With it,
the SDK captures a baseline, then checkpoints after every fifth live tool call
that declares a workspace change, and on completion. Rebuno stores the sandbox
binding and snapshot IDs; Vercel stores the files.

Each new sandbox gets a unique Git branch, and shell commands push the current
branch. Reopening a workspace preserves its branch.

A sandbox session stops after its 10-minute execution limit and keeps its
files, and `open` resumes it. Missing sandboxes or selected snapshots fail the
execution.

## Wrap the tools

`shell` and `write_file` declare workspace changes. `read_file` uses the default
of none:

```python theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
from rebuno import tool
from vercel.sandbox import SandboxPathNotFoundError


@tool("shell", resources=["workspace"])
async def shell(command: str) -> str:
    """Run a shell command in the repository and return its exit code and output."""
    result = await workspace.run_process(
        "bash", ["-c", command], cwd=WORKDIR, kill_after=120, capture_output=True
    )
    return f"exit code {result.returncode}\n{result.stdout}{result.stderr}"[-10000:]


@tool("read_file")
async def read_file(path: str) -> str:
    """Return a file's contents. The path is relative to the repository root."""
    try:
        return await workspace.fs.read_text(f"{WORKDIR}/{path}")
    except SandboxPathNotFoundError:
        return f"{path} does not exist"


@tool("write_file", resources=["workspace"])
async def write_file(path: str, content: str) -> str:
    """Replace a file's contents, creating it if needed. The path is relative to the repository root."""
    await workspace.fs.write_text(f"{WORKDIR}/{path}", content)
    return f"wrote {path}"
```

The model also uses `shell` to commit and push the branch. These tools use
`safe_to_retry`, so an interrupted command can run again. See
[idempotency](/sdk/python/tools#idempotency) when adding commands with effects
that must not repeat.

## Keep credentials out of the sandbox

Cloning and pushing need a GitHub token. The driver creates the sandbox with a
[network policy](https://vercel.com/docs/vercel-sandbox/concepts/firewall)
that sets the `Authorization` header on requests to `github.com` and allows
other domains unchanged:

```python theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
credentials = base64.b64encode(f"x-access-token:{token}".encode()).decode()
policy = NetworkPolicy.custom(
    {
        "*": [],
        "github.com": [
            NetworkPolicyRule(
                transform=[
                    NetworkPolicyTransform(
                        headers={"Authorization": f"Basic {credentials}"}
                    )
                ]
            )
        ],
    }
)
```

git works as usual inside the sandbox, and nothing in it can read the token.
The driver applies the policy when creating or opening a sandbox, so resumed
workspaces use the token the worker currently holds.

Opening the pull request is a separate tool that calls GitHub's API from the
agent:

```python theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
@tool("open_pr", idempotency="at_most_once")
async def open_pr(title: str, body: str) -> str:
    """Open a pull request from the pushed branch."""
    ...
```

The rule covers only `github.com`, so the sandbox can push a branch but cannot
use that token to call the API. Protect the default branch with a GitHub ruleset
that requires a pull request.

## Stop while waiting

The handler stops the workspace when the model loop finishes or waits for
approval:

```python theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
from rebuno import Blocked, execution

result = None
try:
    result = await graph.ainvoke({"messages": [*history, {"role": "user", "content": task}]})
finally:
    if result is not None or isinstance(execution().suspension, Blocked):
        await workspace.pause()
```

`pause()` stops the sandbox. Files persist and running processes end. The check
reads `execution().suspension` because a framework can catch `Blocked` inside
its loop. After approval, the next dispatch replays up to the held call and the
driver resumes the same sandbox. A completion checkpoint briefly resumes a
stopped sandbox and stops it again after capture.

## Sessions

The handler reads the previous completed turn with `previous()` and returns its
conversation in `Result.state`:

```python theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
from langchain_core.messages import messages_to_dict
from rebuno import Result, previous

prior = await previous() or {}
...
return Result(
    output={"answer": result["messages"][-1].text},
    state={
        "messages": messages_to_dict(result["messages"]),
    },
)
```

Executions in the same session reuse the workspace and Git branch, so later
pushes update the same pull request. See [Sessions](/sdk/python/agents#sessions).

## Fork the workspace

```bash theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
rebuno exec fork <execution-id> --at <event-seq> --session fix-tests-fork
```

A fork creates a separate sandbox from the selected snapshot. At an uncovered
point it uses the newest earlier checkpoint; copied tool results still replay
through the requested event and may describe changes missing from that sandbox.
See [Fork coverage](/sdk/python/resources#fork-coverage).

Snapshots restore sandbox files; GitHub branches and pull requests persist.

## Write the policy

```yaml theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
default_action: deny
rules:
  - id: allow-llm
    when:
      step_kind: llm_call
    then:
      decision: allow

  - id: allow-sandbox
    when:
      targets: [shell, read_file, write_file]
    then:
      decision: allow

  - id: open-pr
    when:
      target: open_pr
    then:
      decision: require_approval
      reason: opening a pull request needs approval
```

Sandbox tools are allowed. Opening a pull request waits for approval.

## Run it

[`examples/integrations/sandbox/vercel`](https://github.com/rebuno/rebuno/tree/main/examples/integrations/sandbox/vercel)
has the full agent, resource driver, policy, and dev kernel config.

Set `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, `REPO` (the
repository as `owner/name`), `GITHUB_TOKEN`, `LLM_MODEL`, `LLM_BASE_URL`, and
`LLM_API_KEY`. The token needs read and write access to the repository's
contents and pull requests. Start the kernel and agent from that directory:

```bash theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
cd examples/integrations/sandbox/vercel
rebuno dev --config rebuno.yaml
```

```bash theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
pip install rebuno vercel httpx2 langchain langchain-openai
python agent.py
```

Create an execution:

```bash theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
rebuno exec create vercel '{"task": "The tests are failing. Fix the bug and open a pull request."}' --session fix-tests
```

The `open_pr` call appears in `rebuno exec watch`. See
[Approvals](/policy#approvals) to approve it. Continue the same session with:

```bash theme={"theme":{"light":"min-light","dark":"material-theme-ocean"}}
rebuno exec create vercel '{"task": "Also add a test for the edge case."}' --session fix-tests
```

To see a re-dispatch, stop the agent after a few tool calls and start it again.
The kernel dispatches the execution once its lease expires, two minutes by
default. [`lease_timeout_seconds`](/agents) sets a shorter lease for the agent.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.