Metadata-Version: 2.5
Name: levain-runtime
Version: 0.1.29
Summary: Self-hosted runtime for Levain agents: run an agent's nodes on your own machine or container while the platform drives the recipe
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: certifi>=2026.10.19
Requires-Dist: httpx==0.28.1
Requires-Dist: levain-sdk[graph,models,review]
Requires-Dist: pydantic==2.13.4
Requires-Dist: starlette==1.10.0
Requires-Dist: typer==0.27.3
Requires-Dist: uvicorn==0.54.0
Description-Content-Type: text/markdown

# levain-runtime

Run a [Levain](https://levainlabs.com) agent on your own machine or in your own
container image. The platform keeps the recipe, its versions, the run history
and the improvement loop; this runtime executes the agent's nodes where your
data, tools and dependencies live.

The runtime never accepts inbound connections. It opens one outbound tunnel to
the platform and serves node execution over it. Model calls either go through
the platform (metered as usual) or use the Claude login present on the runtime
host, as configured per runtime.

## Install

```sh
uv tool install levain-runtime
```

## Set up

```sh
# Run nodes directly on this host:
levain-runtime init --mode direct

# Direct mode serving four sessions at once (Linux with bubblewrap):
levain-runtime init --mode direct --sessions 4

# Or run each session in a container built from your own image:
levain-runtime init --mode docker --image ghcr.io/you/agent-runtime:latest
```

Direct mode shows nodes `/workspace`, `/recipes`, `/sdk` and `/sandbox` from
the runtime's home directory (`~/.levain/runtime`). On Linux with bubblewrap
installed each node sees them inside its sandbox and `init` needs no root;
runtimes with different homes (`LEVAIN_RUNTIME_HOME`) can share a filesystem.
Without bubblewrap the command prints and runs `ln` commands linking them at
the filesystem root, with `sudo` when `/` is not writable. On macOS the links
come from `/etc/synthetic.conf`: `init` writes the entries and asks for one
restart.

A docker image needs Python 3.12 or later with `levain-sdk[review,models]`
installed, plus whatever your recipes import.

## Register and run

```sh
export LEVAIN_API_KEY=...   # a workspace API key
levain-runtime register --name workstation --model-auth local
levain-runtime up
```

`register` creates the runtime in your workspace and stores its credentials
here. If the runtime was created elsewhere (the Levain app or API), pass the
credentials it returned instead:
`levain-runtime connect --credentials runtime-credentials.json`.

Re-running `register` with a name the workspace already holds reclaims that
runtime with new credentials; the agents bound to it stay bound. It is refused
while the runtime is online or unreachable, unless you pass `--take-over`.

`up` starts the local node server, the tunnel client and a heartbeat so the
platform can show whether the runtime is online. It fetches the current tunnel
settings from the platform on every start. `levain-runtime status` prints the
configuration and the sessions on this runtime.

## Upgrade

A platform release can need a newer runtime: the node and turn entrypoints
under `/sandbox` ship with this package, and runs wait while the runtime is too
old for them. The Runtimes page in the Levain app, and `runner_version` in
`GET /api/v1/runtimes`, show the version each runtime runs;
`levain-runtime --version` prints it here.

While `up` runs, the platform's heartbeat reply names the version it needs.
`levain-runtime status` prints it (`required: 0.1.15 (upgrade needed)`), and
`GET http://127.0.0.1:17777/v1/health` carries `version`, `required_version`
and `upgrade_needed`, so whatever keeps the runtime running can upgrade it
before any run waits.

```sh
uv tool upgrade levain-runtime
systemctl --user restart levain-runtime  # or however `up` runs on this host
```

`up` rewrites `/sandbox` from the installed release on every start, so an
upgrade needs no new `init`. Restarting ends the nodes the runtime was running,
so upgrade between runs.

## How it works

Each agent session gets a workspace directory under the runtime home
(`sessions/<id>/workspace`, or `agents/<id>/workspace` for agents with a
persistent workspace). The platform delivers the recipe and SDK into `/recipes`
and `/sdk`, then runs each node as a short-lived process with the session's
environment. Everything a node prints is streamed back and lands in the run's
logs on the platform.

Direct mode serves as many sessions at once as the machine can isolate, or
up to `--sessions N`: each node runs under bubblewrap with its session's
folders mounted at `/workspace`, `/recipes/active` and `/sdk/active`; the real
workspace path is in `LEVAIN_HOST_WORKSPACE` for tools outside the sandbox
such as Docker. Without bubblewrap it serves one session at a time, with
`/workspace` linked to the active session. Docker mode starts one container
per session and has no such limit.

## Sessions and your Claude login

In direct mode an agent's turns run in the session's workspace directory with
this machine's Claude login and config dir (`CLAUDE_CONFIG_DIR`, or
`~/.claude`). Claude Code files the agent's sessions where it files yours, under
that directory's entry, so from the workspace directory `claude --resume` lists
the agent's sessions next to your own and continues any of them.
`levain-runtime status` prints each session's workspace and the Claude session
it continues.

The map from a Levain session to its Claude session lives in
`<config dir>/levain-sessions/`. To have an agent continue a session you started
yourself in its workspace directory:

```sh
levain-runtime adopt <levain session id> <claude session id>
```

The agent's next turn resumes that session. Docker mode copies your login into
each container instead; its sessions stay in the container.

## Per-turn environment

In direct mode the runtime reads `env.json` in its home before every node it
runs, so you can change a node's environment between turns without a restart.
For example, you can point one agent at another Claude login:

```json
{
  "all": { "HTTP_PROXY": "http://127.0.0.1:3128" },
  "agents": {
    "<agent id>": { "CLAUDE_CONFIG_DIR": "/home/me/.claude-second" }
  }
}
```

An agent's entry wins over `all`, and both win over anything the platform sends
for the session or the turn. The session map follows the `CLAUDE_CONFIG_DIR` a
turn gets, as do `status` and `adopt`. To switch the value a turn starts with,
write the new file next to it and rename it into place.

The file must belong to the user the runtime runs as, and no one else may be
able to write it (`chmod 600`). If it is not, or it is malformed, nodes fail
with an error that names the file, never its values. The values stay on this
machine: they are not logged and not sent to the platform. Without the file,
nodes get the runtime's own environment. Docker mode ignores the file.

## Waiting sessions

In direct mode, when an agent's turn stops to wait, the platform gives the
session's slot back and tells the runtime what the session waits on. The
runtime keeps one file per waiting session, `waiting/<session id>.json` under
its home, so a process on this machine can react without polling the API:

```json
{
  "session_id": "…",
  "agent_id": "…",
  "workspace_id": "…",
  "run_id": "…",
  "reason": "user_input",
  "payload": { "question": "…" },
  "since": "2026-10-29T08:12:44.018311+00:00"
}
```

`reason` is `user_input` (a person's reply), `child_agent` (another agent's
run) or `external` (an outside event); `payload` is what the agent passed with
the wait. Each file is renamed into place, so a watcher of `*.json` only sees
whole files. It is removed when the session resumes, is provisioned again or
is deleted; a restart of the runtime keeps it.
