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

# Gemini Managed Agents

> Google runs the Antigravity agent loop. Every tool call runs inside a boxd machine you control.

![The Gemini sparkle and wordmark inside the boxd card.](https://mintcdn.com/azin/zoyN5E3fRLo4ubcc/images/boxd-x-gemini-agents.png?fit=max\&auto=format\&n=zoyN5E3fRLo4ubcc\&q=85\&s=e417d1e32495cd2efea608095322cc0f)

[Gemini Managed Agents](https://aistudio.google.com/docs/agent-environment), such as the Antigravity agent, run through the Gemini Interactions API. Google hosts the model and the agent loop. A [self-hosted environment](https://github.com/google-gemini/gemini-api-cli/tree/main/docs/self-hosted) moves tool execution to you, so every `execute_bash`, file read, file write and search runs on infrastructure you own.

The worker is the environment runtime in Google's [`gemini-api` CLI](https://github.com/google-gemini/gemini-api-cli). Run it on boxd and the agent's commands run on a full Linux machine with its own kernel. The machine hibernates with its files and processes in place, a fresh one starts from a snapshot for every interaction if you want that, and each has an egress allowlist you set from outside it.

## How it fits together

The worker talks to Google over outbound HTTPS only. Nothing dials into the machine, so it needs no open ports.

| Party | What it does |
| - | - |
| **Google** | Runs the model and the agent loop, and holds your self-hosted *environment*: an id, an environment key, and a queue of interactions waiting for a worker. |
| **Your app** | Creates interactions in that environment with your Gemini API key. |
| **boxd** | Runs the worker that executes the agent's tool calls: one long-lived machine, or a fresh machine per interaction. |

The runtime has two modes. Use one per environment: if both run against the same environment, they compete for its interactions.

| | One worker machine | A machine per interaction |
| - | - | - |
| Runtime command | `gemini-api environments connect` | `gemini-api environments poll` on your host, `serve` on the machine |
| Files in `/workspace` | stay between interactions, also while the machine hibernates | start empty every time |
| Credential on the machine | the environment key | only the interaction's short-lived lease token |
| Suits | one user, a development loop | several users, untrusted input, production |

## Set up the Gemini side

Create a self-hosted environment with your [Gemini API key](https://ai.google.dev/gemini-api/docs/api-key). The response holds the environment's `id` and an `environment_key`, which the API returns only once:

```bash theme={"theme":"github-dark"}
curl -X POST "https://generativelanguage.googleapis.com/v1beta/environments" \
  -H "x-goog-api-key: $GEMINI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode": "self_hosted"}'

export GEMINI_ENVIRONMENT_ID="..."   # "id", without an "environments/" prefix
export GEMINI_ENVIRONMENT_KEY="..."  # "environment_key"
```

The environment key is scoped to this environment: it is what a worker uses to lease and run its interactions. Keep your Gemini API key with your app.

The examples below use the [boxd Python SDK](https://docs.boxd.sh/reference/python-sdk.md) (`pip install boxd`, with `BOXD_API_KEY` set). Both setups install the same pinned `gemini-api` release and check it against the release's checksums. Save this next to the scripts as `install-gemini-api.sh`:

```bash install-gemini-api.sh theme={"theme":"github-dark"}
set -eu
v=0.4.6
arch=$(uname -m | sed -e s/aarch64/arm64/)
file="gemini-api_Linux_$arch.tar.gz"
base="https://github.com/google-gemini/gemini-api-cli/releases/download/v$v"

cd "$(mktemp -d)"
curl -fsSLO "$base/$file"
curl -fsSLO "$base/checksums.txt"
grep " $file$" checksums.txt | sha256sum -c -

sudo tar -xzf "$file" -C /usr/local/bin gemini-api
sudo install -d -o boxd -g boxd /workspace
```

## One worker machine

The worker starts in its own session, so it keeps running after the exec returns:

```python worker.py theme={"theme":"github-dark"}
import os
from pathlib import Path

from boxd import Boxd

INSTALL = Path("install-gemini-api.sh").read_text()
CONNECT = "gemini-api environments connect --root-dir /workspace --workdir /workspace"

boxd = Boxd()  # BOXD_API_KEY from the environment
machine = boxd.machines.create(
    "gemini-worker",
    isolated=True,
    idle_ignore_outbound=False,  # the worker's polling keeps the machine awake
)
boxd.machines.wait_until_ready(machine.id)
print(boxd.machines.exec(machine.id, ["bash", "-c", INSTALL], timeout=300).stdout)

# Only the Gemini API is reachable from now on. Set from outside: nothing inside can change it.
boxd.machines.set_egress_allow(machine.id, ["generativelanguage.googleapis.com"])

# The environment key goes to the worker's environment, not into the command or onto the disk.
boxd.machines.exec(
    machine.id,
    ["bash", "-c", f"setsid {CONNECT} > ~/worker.log 2>&1 < /dev/null &"],
    env={"GEMINI_ENVIRONMENT_KEY": os.environ["GEMINI_ENVIRONMENT_KEY"]},
)
print(f"worker started on {machine.name} ({machine.id})")
```

Send the agent a task from anywhere, with your Gemini API key:

```bash theme={"theme":"github-dark"}
gemini-api agent run "Create hello.txt and run uname -a in bash." \
  --agent antigravity-preview-09-2026 \
  --body "{\"environment\": \"$GEMINI_ENVIRONMENT_ID\"}"
```

```text theme={"theme":"github-dark"}
Created `hello.txt` and executed `uname -a`.
Linux gemini-worker 6.18.0+ #1 SMP PREEMPT_DYNAMIC ... x86_64 GNU/Linux
```

The host name is the machine's name: the command ran on boxd. The worker's log is in `~/worker.log` on the machine.

### Sleep between uses

The worker polls Google every few seconds, and that outbound traffic keeps the machine awake. When you don't need it, [hibernate](https://docs.boxd.sh/guides/suspend-resume.md) it. Interactions you send meanwhile wait at Google, and the worker picks them up when the machine wakes, with its files and its worker process as they were:

```python theme={"theme":"github-dark"}
import time

from boxd import Boxd

boxd = Boxd()
machine_id = boxd.machines.get("gemini-worker").id

boxd.machines.hibernate(machine_id)
deadline = time.monotonic() + 120
while boxd.machines.get(machine_id).status != "hibernated" and time.monotonic() < deadline:
    time.sleep(0.5)

boxd.machines.wake(machine_id)
boxd.machines.wait_until_ready(machine_id)
print(boxd.machines.exec(machine_id, ["cat", "/workspace/hello.txt"]).stdout)
```

Nothing wakes a hibernated machine on its own when an interaction arrives, so wake it before you send work, or use a machine per interaction.

## A machine per interaction

Here the poller runs on a host you control and holds the environment key. For each interaction it leases, it runs a provisioning command with a JSON payload on stdin. The command below starts a fresh isolated machine from a [snapshot](https://docs.boxd.sh/guides/snapshots.md) and runs `gemini-api environments serve` on it with only that interaction's lease token.

### Build the snapshot once

```python snapshot.py theme={"theme":"github-dark"}
import time
from pathlib import Path

from boxd import Boxd

INSTALL = Path("install-gemini-api.sh").read_text()

boxd = Boxd()
builder = boxd.machines.create("gemini-builder", isolated=True, delete_after=3600)
try:
    boxd.machines.wait_until_ready(builder.id)
    print(boxd.machines.exec(builder.id, ["bash", "-c", INSTALL], timeout=300).stdout)
    boxd.snapshots.create(builder.id, "gemini-worker-base")
    while boxd.snapshots.get("gemini-worker-base").status != "ready":
        time.sleep(2)
finally:
    boxd.machines.delete(builder.id)
```

Add whatever every interaction should start with: a toolchain, private packages, a repository.

### The provisioning command

```python provision.py theme={"theme":"github-dark"}
import json
import subprocess
import sys
import time
import uuid
from pathlib import Path

from boxd import Boxd

SERVE = (
    'setsid gemini-api environments serve --interaction-id "$IID" --lease-token "$LEASE" '
    "--root-dir /workspace --workdir /workspace > ~/serve.log 2>&1 < /dev/null &"
)

payload = json.load(sys.stdin)
boxd = Boxd()  # BOXD_API_KEY from the environment
machine = boxd.machines.create(
    f"gemini-{uuid.uuid4().hex[:8]}",
    from_snapshot="gemini-worker-base",
    isolated=True,
    labels={"gemini.interaction_id": payload["interaction_id"]},
    idle_ignore_outbound=False,  # the worker's own traffic keeps it awake
    delete_after=7200,  # the safety net: boxd deletes it after 2 hours
)
try:
    boxd.machines.wait_until_ready(machine.id, timeout=60)
    boxd.machines.set_egress_allow(machine.id, ["generativelanguage.googleapis.com"])
    # The machine gets the interaction id and its lease token, nothing else.
    env = {"IID": payload["interaction_id"], "LEASE": payload["lease_token"]}
    boxd.machines.exec(machine.id, ["bash", "-c", SERVE], env=env, timeout=20)

    attached = "grep -q 'Attached interaction session' ~/serve.log"
    deadline = time.monotonic() + 45
    while boxd.machines.exec(machine.id, ["bash", "-c", attached]).exit_code != 0:
        if time.monotonic() > deadline:
            raise TimeoutError("the worker did not attach in 45 seconds")
        time.sleep(1)
except BaseException:
    boxd.machines.delete(machine.id)
    raise

# The worker holds the lease now. cleanup.py deletes the machine when the worker exits.
cleanup = Path(__file__).with_name("cleanup.py")
subprocess.Popen([sys.executable, cleanup, machine.id], start_new_session=True,
                 stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
```

```python cleanup.py theme={"theme":"github-dark"}
import sys
import time

from boxd import Boxd

boxd, machine_id = Boxd(), sys.argv[1]
while boxd.machines.exec(machine_id, ["pgrep", "-x", "gemini-api"]).exit_code == 0:
    time.sleep(10)
boxd.machines.delete(machine_id)
```

Run the poller with it, on a host with the `gemini-api` CLI, `BOXD_API_KEY` and the environment key:

```bash theme={"theme":"github-dark"}
gemini-api environments poll --provision-cmd "python provision.py"
```

Every interaction now gets its own machine. Send several at once and they run in parallel:

```text theme={"theme":"github-dark"}
gemini-3aaa09e0
0
```

That is `hostname; ls -A /workspace | wc -l` from one interaction: its own machine, and an empty workspace.

The poller kills a provisioning command that runs longer than `--provision-timeout` (2 minutes by default). If that happens after the machine exists, or the cleanup loop dies, `delete_after` removes the machine after 2 hours.

## What the agent can reach

* **Its own VM.** Every machine here is created `isolated`: its own kernel, no private network with your other machines, and no boxd credentials inside it. See [machine specs](https://docs.boxd.sh/guides/machine-specs.md).
* **Only the Gemini API.** The [egress allowlist](https://docs.boxd.sh/guides/egress-allowlist.md) is set through the boxd API, so nothing inside the machine can change it. Add the hosts the agent needs, such as `pypi.org` and `files.pythonhosted.org` for `pip install`.
* **A lease token, not a key, per interaction.** The per-interaction machine never holds your Gemini API key or the environment key. The agent's commands can read the lease token, but it works only for the interaction they are already part of and expires with it.
* **The environment key, on the one worker machine.** It sits in the worker process's environment, not on disk. The agent's commands run as the same user, so treat it as readable, and use a machine per interaction for untrusted input.

## Read more

* [Gemini Managed Agents environments](https://aistudio.google.com/docs/agent-environment)
* [`gemini-api` self-hosted environments](https://github.com/google-gemini/gemini-api-cli/tree/main/docs/self-hosted)
* [Claude Managed Agents on boxd](https://docs.boxd.sh/use-cases/claude-managed-agents.md) and [the OpenAI Agents API on boxd](https://docs.boxd.sh/use-cases/openai-agents-api.md), the same split for other agents
* [Egress allowlist](https://docs.boxd.sh/guides/egress-allowlist.md)


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