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

# Run on Your Own Machine

> Run the TestDriver runner on a laptop, desktop, or VM you control and drive it from your tests.

TestDriver normally provisions a cloud sandbox for you. You can instead run the **runner** on a machine you own: a developer laptop, an office Mac, a lab Windows box, or a VM in your own data center. Your tests then drive that machine instead of a cloud sandbox.

This is useful when the machine under test cannot be replicated in the cloud: specific hardware, a licensed desktop application, a device plugged into USB, or an app that only reaches an internal network.

```mermaid theme={null}
graph LR
    A[Your Test] --> B[TestDriver API]
    B --> C[Ably Channel]
    C --> D[Runner on your machine]
    D --> C
```

<Note>
  The runner only makes **outbound** connections, to the TestDriver API and to Ably. You do not need to open an inbound port or expose the machine to the internet.
</Note>

## Choose your path

| Your machine | Use |
| - | - |
| Windows EC2 instance in your AWS account | [AWS Setup Guide](/aws-setup). Instances are spawned and provisioned automatically. |
| Any other machine (macOS, Linux, Windows, on-prem VM) | This guide. You start the runner yourself. |

<Tip>
  If you want unlimited execution, assisted setup, and custom VM images, see [Self-Hosted (Enterprise)](/self-hosted).
</Tip>

## Prerequisites

Every machine that runs the runner needs:

* **Node.js 18 or newer**
* **Python 3** with `pyautogui` and `Pillow`
* **A real, unlocked desktop session.** The runner drives the mouse and keyboard, so the screen must be awake and not locked. Disable screen savers, sleep, and lock timers.
* **Google Chrome** on the `PATH`, if your tests launch a browser
* Outbound HTTPS and WebSocket access to `api.testdriver.ai` and `*.ably.io`

Install the Python dependencies:

<Tabs>
  <Tab title="macOS">
    ```bash theme={null}
    brew install python
    pip3 install pyautogui Pillow
    ```
  </Tab>

  <Tab title="Linux">
    ```bash theme={null}
    sudo apt-get install -y python3-pip python3-xlib
    pip3 install pyautogui Pillow
    ```
  </Tab>

  <Tab title="Windows">
    ```powershell theme={null}
    pip install pyautogui Pillow
    ```
  </Tab>
</Tabs>

### Platform notes

<AccordionGroup>
  <Accordion title="macOS: grant Accessibility and Screen Recording permissions">
    macOS blocks synthetic input and screen capture until you allow it explicitly. Without this the runner connects but every click and screenshot silently fails.

    Open **System Settings → Privacy & Security** and add your terminal application (Terminal, iTerm, or whichever app starts the runner) to **both**:

    * **Accessibility** (lets the runner move the mouse and type)
    * **Screen Recording** (lets the runner take screenshots)

    Quit and reopen the terminal after granting permissions, otherwise the old process keeps the old denial.

    On Retina displays, screenshots are captured at physical pixels and downscaled to logical points so that coordinates returned by the AI line up with where clicks actually land. No configuration is needed.
  </Accordion>

  <Accordion title="Linux: provide a display">
    The runner needs an X11 display. On a desktop machine, set `DISPLAY` to your existing session. On a headless server, start a virtual framebuffer first:

    ```bash theme={null}
    sudo apt-get install -y xvfb xfce4 xfce4-terminal dbus-x11 wmctrl
    Xvfb :0 -screen 0 1366x768x24 &
    export DISPLAY=:0
    ```

    To watch the session, add a VNC server and noVNC:

    ```bash theme={null}
    sudo apt-get install -y x11vnc novnc websockify
    x11vnc -display :0 -forever -shared &
    ```

    Disable screen blanking so the desktop stays visible:

    ```bash theme={null}
    xset s off && xset s noblank && xset -dpms
    ```
  </Accordion>

  <Accordion title="Windows: keep the console session active">
    Run the runner in a console or RDP session that stays signed in. If the session locks or disconnects, the desktop is torn down and automation stops. Configure the machine to auto-logon and disable the lock screen timeout.
  </Accordion>
</AccordionGroup>

## Step 1: Install the runner

```bash theme={null}
npm install -g @testdriverai/runner
```

This installs two commands. You use `testdriver-sandbox-agent`, which connects your machine to a TestDriver session.

Verify the install:

```bash theme={null}
testdriver-sandbox-agent --help || echo "installed"
```

## Step 2: Register the machine

Ask the API for a session bound to your machine's IP address:

```bash theme={null}
curl -X POST https://api.testdriver.ai/api/v7/sandbox/authenticate \
  -H "Content-Type: application/json" \
  -d '{
    "apiKey": "your_api_key",
    "ip": "192.168.1.50",
    "os": "linux",
    "keepAlive": 900000
  }'
```

The response contains the sandbox ID and the Ably credentials the runner needs:

```json theme={null}
{
  "success": true,
  "sandboxId": "sb-69cce96e-1e0",
  "agent": {
    "token": {
      "token": "aFC5YQ.Ki9G9...",
      "keyName": "aFC5YQ.ZIdTww",
      "issued": 1791908438832,
      "expires": 1791930038832,
      "capability": "{\"testdriver:...:sb-69cce96e-1e0\":[\"history\",\"presence\",\"publish\",\"subscribe\"]}",
      "clientId": "agent-sb-69cce96e-1e0"
    },
    "channel": "testdriver:...:sb-69cce96e-1e0"
  }
}
```

<Note>
  Pass `os: "linux"` on macOS. The value only describes the session to the API; the runner detects the real platform itself.
</Note>

### Write the config file

The runner reads its credentials from a JSON file on disk. Copy `sandboxId` and the whole `agent` block into it:

| Platform | Config file path |
| - | - |
| macOS / Linux | `/tmp/testdriver-agent.json` |
| Windows | `C:\Windows\Temp\testdriver-agent.json` |

```json /tmp/testdriver-agent.json theme={null}
{
  "sandboxId": "sb-69cce96e-1e0",
  "apiRoot": "https://api.testdriver.ai",
  "apiKey": "your_api_key",
  "ably": {
    "token": {
      "token": "aFC5YQ.Ki9G9...",
      "keyName": "aFC5YQ.ZIdTww",
      "issued": 1791908438832,
      "expires": 1791930038832,
      "capability": "{\"testdriver:...:sb-69cce96e-1e0\":[\"history\",\"presence\",\"publish\",\"subscribe\"]}",
      "clientId": "agent-sb-69cce96e-1e0"
    },
    "channel": "testdriver:...:sb-69cce96e-1e0"
  }
}
```

| Field | Source | Purpose |
| - | - | - |
| `sandboxId` | `sandboxId` from the response | Identifies this session. Your test attaches to it. |
| `apiRoot` | The API you called | Where the runner uploads screenshots |
| `apiKey` | Your team API key | Authenticates those uploads |
| `ably.token` | `agent.token` from the response, copied verbatim | Realtime credentials |
| `ably.channel` | `agent.channel` from the response | Channel the runner listens on |

To put the file somewhere else, set `CONFIG_PATH` to its full path before starting the runner.

<Warning>
  This file holds a session token and your API key. Keep it readable only by the user running the runner, do not commit it, and delete it when the machine is no longer in use.
</Warning>

## Step 3: Start the runner

```bash theme={null}
testdriver-sandbox-agent
```

You should see the runner connect and announce itself:

```
[agent] TestDriver Sandbox Agent starting...
[agent] Sandbox ID: sb-69cce96e-1e0
[agent] Transport: Ably
[agent] Automation module initialized
[realtime] Publishing control: type=runner.ready
[agent] Agent ready — listening for commands via Ably
```

Leave this process running. On Linux, set `DISPLAY` first:

```bash theme={null}
DISPLAY=:0 testdriver-sandbox-agent
```

## Step 4: Point your test at the machine

Pass the machine's `ip` and the sandbox ID from step 2:

```javascript tests/my-machine.test.mjs theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";

describe("My machine", () => {
  it("runs against my own hardware", async (context) => {
    const testdriver = TestDriver(context, { newSandbox: false });

    await testdriver.connect({
      ip: process.env.TD_MACHINE_IP,
      sandboxId: process.env.TD_SANDBOX_ID,
    });

    const button = await testdriver.find("Sign In button");
    await button.click();

    const result = await testdriver.assert("the dashboard is visible");
    expect(result).toBeTruthy();
  });
});
```

Run it:

```bash theme={null}
TD_MACHINE_IP=192.168.1.50 TD_SANDBOX_ID=sb-69cce96e-1e0 vitest run
```

<Note>
  `newSandbox: false` stops the SDK from provisioning a cloud sandbox. Without it, your test runs in the cloud and ignores your machine.
</Note>

<Note>
  Each config file covers one session. The Ably token in it expires (see its `expires` field), so repeat steps 2 and 3 to get fresh credentials before the next run.
</Note>

## Logs

| Platform | Runner log |
| - | - |
| macOS / Linux | `/tmp/testdriver-agent.log` |
| Windows | `C:\Windows\Temp\testdriver-agent.log` |

The runner also writes a status file next to the log (`testdriver-agent-status.json`) with the sandbox ID, process ID, detected IP, and start time.

## Security

* **Use a dedicated machine or user account.** The runner can click anything on the desktop and run shell commands sent by your tests. Do not point it at a machine holding personal credentials or production access.
* **Treat the config file as a secret.** It contains a session token and your API key.
* **Do not expose VNC to the internet.** If you add VNC for debugging, bind it to localhost or your VPN and tunnel in over SSH.
* **Rotate your API key** if a machine is decommissioned or shared.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Sandbox has expired and is no longer available">
    The session ended, which happens when the runner stops or your test disconnects. Repeat step 2 to get a fresh sandbox ID and token, update the config file, restart the runner, and use the new ID in your test.
  </Accordion>

  <Accordion title="Runner starts but clicks and screenshots do nothing (macOS)">
    Accessibility or Screen Recording permission is missing. Add your terminal application to both lists under **System Settings → Privacy & Security**, then fully quit and reopen the terminal.
  </Accordion>

  <Accordion title="Waiting for config file">
    The runner cannot find `/tmp/testdriver-agent.json` (or `C:\Windows\Temp\testdriver-agent.json`). Write the config file on the same machine and readable by the same user that starts the runner. To use a different location, set `CONFIG_PATH`.
  </Accordion>

  <Accordion title="Test provisions a cloud sandbox instead of using my machine">
    Pass `newSandbox: false` to `TestDriver()` and make sure both `ip` and `sandboxId` reach `connect()`. If either is undefined, the SDK falls back to cloud provisioning.
  </Accordion>

  <Accordion title="Invalid API key">
    Confirm `TD_API_KEY` matches the environment in `TD_API_ROOT`. Keys are scoped per environment. Get your key at [console.testdriver.ai/team](https://console.testdriver.ai/team).
  </Accordion>

  <Accordion title="Screenshots are blank or show a frozen desktop (Linux)">
    `DISPLAY` points at a display with no desktop running, or the screen blanked. Start a desktop on that display and disable blanking with `xset s off && xset s noblank && xset -dpms`.
  </Accordion>
</AccordionGroup>

## Environment variables

| Variable | Default | Purpose |
| - | - | - |
| `TD_API_KEY` | Required | Your team API key |
| `TD_API_ROOT` | `https://api.testdriver.ai` | API server to register with |
| `CONFIG_PATH` | `/tmp/testdriver-agent.json` | Override the runner config file location |
| `DISPLAY` | `:0` | X11 display (Linux) |
| `SENTRY_DSN` | Unset | Error reporting endpoint |


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