Metadata-Version: 2.5
Name: agentplane-control-plane
Version: 0.2.1
Summary: Shared AgentPlane control plane: Chronicle envelopes + TokenOps governance over HTTP.
Project-URL: Homepage, https://github.com/theagentplane/control-plane
Project-URL: Repository, https://github.com/theagentplane/control-plane
Project-URL: Issues, https://github.com/theagentplane/control-plane/issues
Project-URL: Changelog, https://github.com/theagentplane/control-plane/blob/main/CHANGELOG.md
Author: Susheem Koul, Tisha Chawla
License-Expression: MIT
License-File: LICENSE
Keywords: agents,chronicle,control-plane,governance,llm,observability,tokenops
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.27
Requires-Dist: jinja2>=3.1
Requires-Dist: platformdirs>=4.0
Requires-Dist: psutil>=5.9
Requires-Dist: pydantic>=2
Requires-Dist: pyyaml>=6
Requires-Dist: uvicorn[standard]>=0.32
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff==0.15.22; extra == 'dev'
Requires-Dist: twine>=6; extra == 'dev'
Provides-Extra: ui
Requires-Dist: pandas>=2; extra == 'ui'
Requires-Dist: streamlit>=1.32; extra == 'ui'
Description-Content-Type: text/markdown

<div align="center">

# AgentPlane Control

**The shared control plane for agent traces and spend.**<br>
Chronicle and TokenOps talk HTTP. Only this process owns SQLite. Agents never open the database file.

[![CI](https://github.com/theagentplane/control-plane/actions/workflows/ci.yml/badge.svg)](https://github.com/theagentplane/control-plane/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/agentplane-control-plane.svg)](https://pypi.org/project/agentplane-control-plane/)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://github.com/theagentplane/control-plane)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Status](https://img.shields.io/badge/status-0.x%20%7C%20draft-7B61FF?style=flat-square)](https://semver.org/)
[![Stars](https://img.shields.io/github/stars/theagentplane/control-plane?style=flat&color=yellow)](https://github.com/theagentplane/control-plane/stargazers)

<br>

<img src="docs/assets/ui-tokenops.png" alt="AgentPlane Control TokenOps tab: budgets, policies, a halted evaluator-optimizer run, and recent runs" width="720" />

<sub><i>One UI, one SQLite: budgets and policies on the left, a run halted in-path by <code>progress_guard</code> on the right. Chronicle traces live in the next tab.</i></sub>

</div>

<br>

AgentPlane Control is the **HTTP plane** Chronicle and TokenOps share. Sidecars post envelopes and register runs; the plane stores them; Admin, Chronicle, and TokenOps tabs read the same file. No NFS, no two processes fighting over WAL, no "which DB did that agent open."

**[Why](#why-agentplane-control) · [Architecture](#architecture) · [Install](#install) · [Quick start](#quick-start) · [Sidecars](#sidecars) · [Comparison](#how-it-compares) · [Design](docs/DESIGN.md)**

## Why AgentPlane Control

- **SQLite lives here. Nowhere else.** Chronicle and TokenOps are HTTP clients. They never receive a DB path.
- **One pane for both products.** Admin keys, Chronicle waterfalls, TokenOps budgets / policies / breaches — same process, same file.
- **Ingest is a contract, not a dump.** Chronicle writes `POST /v1/envelopes:batch` (`batch_size=1` = flush now) and replays `GET /v1/traces/{id}/envelopes`. TokenOps registers runs, ledger, and governance over `/v1/*`.
- **Auth when you need it.** Empty key table = local anonymous (all scopes, tenant `local`). Create sidecar keys in Admin, or seed `CONTROL_PLANE_API_KEYS`.
- **pip in, serve, done.** No Postgres, no login screen, no separate dashboard server.

## Architecture

The plane is one FastAPI process. Agents stay agents.

```mermaid
flowchart LR
    subgraph AGENTS["Agent processes"]
      C["Chronicle sidecar<br/>RemoteStore"]
      T["TokenOps sidecar<br/>HttpStore"]
    end

    subgraph PLANE["Control plane (:8800)"]
      API["HTTP /v1"]
      UI["Admin · Chronicle · TokenOps"]
      DB[("SQLite CONTROL_PLANE_DB")]
      API --> DB
      UI --> API
    end

    C -->|"POST /v1/envelopes:batch"| API
    C -->|"GET /v1/traces/{id}/envelopes"| API
    T -->|"POST /v1/runs · ledger · governance"| API
```

| Piece | Owns | Does not own |
|---|---|---|
| **This plane** (`control-plane serve`) | SQLite, HTTP API, HTML UI | Agent loops, LLM calls, record-and-replay, in-path halt |
| **[Chronicle](https://github.com/theagentplane/chronicle)** sidecar | Boundaries, envelopes, fixtures | The database file |
| **[TokenOps](https://github.com/theagentplane/tokenops)** sidecar | `tokenops_run`, `wrap_complete`, ledger client | The database file |

Design notes: [`docs/DESIGN.md`](docs/DESIGN.md).

<img src="docs/assets/ui-chronicle.png" alt="AgentPlane Control Chronicle tab: newest traces first, filter by id or dims" width="720" />

<sub><i>Chronicle tab: newest traces first. Click a row for the time-based waterfall.</i></sub>

## Install

```bash
pip install agentplane-control-plane
```

**Prerequisites:** Python 3.10+. Sidecars are separate packages: `agent-chronicle>=0.4.0`, `agent-tokenops>=0.2.0`.

PyPI name is `agentplane-control-plane`; import is `control_plane`; CLI is `control-plane`.
See [`RELEASING.md`](RELEASING.md) for releases.

Every command below has an equivalent module form that needs nothing on `PATH`:

```bash
control-plane status            # console script
python -m control_plane status  # same thing, always available
```

### If `control-plane` isn't found

`pip` drops the console script into your interpreter's scripts directory
(`Scripts\` on Windows, `bin/` elsewhere) and **cannot** add that directory to
`PATH` — no Python package can, so this is not something we can fix from our end.
pip usually prints a warning when it happens; it is easy to miss.

Three ways out, best first:

```bash
pipx install agentplane-control-plane   # isolated venv + a bin dir already on PATH
uv tool install agentplane-control-plane
python -m control_plane serve           # no install change; works immediately
```

Or put the directory on `PATH` yourself — `python -c "import sysconfig; print(sysconfig.get_path('scripts'))"`
prints the one to add.

This bites hardest on Windows with the [Python Install Manager](https://docs.python.org/3/using/windows.html),
which puts only its `python.exe` shim on `PATH` and leaves each interpreter's
`Scripts\` off it.

### After upgrading Python

Installs belong to one interpreter. A new Python is a new, empty `site-packages`,
so **both** forms stop working after an upgrade — `control-plane` as "command not
found", `python -m control_plane` as the clearer `No module named control_plane`.
Reinstall into the new interpreter:

```bash
python -m pip install --upgrade agentplane-control-plane
```

If you added a scripts directory to `PATH` by hand, note that it is usually
version-scoped (e.g. `...\pythoncore-3.14-64\Scripts`) and will need updating too.
`pipx` and `uv` avoid this by pinning their own interpreter.

From a clone:

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
```

## Quick start

```bash
control-plane start          # runs in the background; safe to re-run (no-op if already up)
control-plane status         # pid, port, version, /health
control-plane stop
```

Open [http://127.0.0.1:8800/](http://127.0.0.1:8800/) — Admin, Chronicle, TokenOps. No login.

One managed instance per machine user; `start`/`stop`/`status` track it via a small
state file (`platformdirs` user-state dir) and a PID — `stop` never touches a process
it didn't start. Logs go to a file next to the state (path printed by `start`), since
the process is detached from your terminal.

Prefer the foreground, un-managed form for scripting, containers, or when you want
Ctrl-C to stop it:

```bash
control-plane serve --port 8800 --db control_plane.db
```

`control-plane ui` is a pointer, not a second server: the HTML UI is served with the API.

## Docker

For hosting (a shared team plane, a demo environment) rather than a local dev loop:

```bash
docker compose up -d      # builds the image, starts on :8800, persists SQLite in a volume
docker compose logs -f
docker compose down       # add -v to also drop the data volume
```

Or without Compose:

```bash
docker build -t agentplane-control-plane .
docker run -d --name control-plane -p 8800:8800 -v control-plane-data:/data agentplane-control-plane
```

The image's entrypoint is `control-plane serve` (foreground, PID 1) — **not** `start`.
Docker/Kubernetes is already the process supervisor here (restart policy, health
checks via the built-in `HEALTHCHECK` hitting `/health`, log collection from
stdout/stderr); `start`/`stop`/`status` are for running the plane directly on a
developer's machine, where nothing else is supervising the process. Don't run them
inside the container — a background/detached mode would exit PID 1 as soon as it
spawned its child, and the container would exit with it.

Set `CONTROL_PLANE_API_KEYS` before exposing the container beyond localhost — the
default (empty) is anonymous, all-scopes access.

## Sidecars

Point both libraries at the same origin. Do **not** set a SQLite path on the agents.

```bash
export CONTROL_PLANE_URL=http://127.0.0.1:8800
# TokenOps also honors TOKENOPS_URL; leave TOKENOPS_EMBEDDED unset
```

**Chronicle** — flush every envelope to the plane:

```python
import os
import chronicle
from chronicle import RemoteStore

store = RemoteStore(os.environ["CONTROL_PLANE_URL"], batch_size=1)
with chronicle.record("my-run", store=store):
    ...
```

**TokenOps** — register runs and share the ledger over HTTP:

```python
from tokenops import ControlPlaneClient, tokenops_run

client = ControlPlaneClient.from_env()  # CONTROL_PLANE_URL or TOKENOPS_URL
with tokenops_run(client=client) as bound:
    ...
```

Auth off until you create a key. Then:

```bash
export CONTROL_PLANE_API_KEYS='chron:local:ingest+read,tops:local:ingest+read,admin:local:read+admin'
export CONTROL_PLANE_API_KEY=chron   # Chronicle / TokenOps sidecar
```

Or create keys in the Admin tab (secret shown once).

## API callers

Every route has one caller class. Agents do not scrape the UI.

| Route | Caller |
|---|---|
| `POST /v1/envelopes:batch` | Chronicle sidecar (only ingest API) |
| `GET /v1/traces/{id}/envelopes` | Chronicle sidecar (fixture replay) and Chronicle waterfall |
| `GET /v1/traces` | Chronicle UI search |
| `POST /v1/runs`, ledger, governance, run-records | TokenOps sidecar |
| segments / budgets / policies, admin keys | UI |

## How it compares

This is not a gateway, not a SaaS, and not a replacement for Chronicle or TokenOps. It is the **shared store + UI** those two already assume.

| | AgentPlane Control | TokenOps embedded SQLite | Langfuse / Phoenix |
|---|:---:|:---:|:---:|
| Primary focus | Shared plane (traces + spend) | Governance in one process | Observe / traces |
| Agents open the DB file | No | Yes (same `TOKENOPS_DB`) | N/A (hosted or collector) |
| Chronicle + TokenOps one UI | Yes | TokenOps UI only | No |
| In-path halt / mutate | Via TokenOps sidecar | Yes | No (analytics) |
| Record-and-replay fixtures | Via Chronicle sidecar | No | No |
| Requires hosted SaaS | No | No | Often |

What this does **not** do: call models, wrap `complete`, record boundaries, or host a multi-tenant cloud for you. Fail-closed auth is opt-in (create keys). The HTTP contract is still 0.x.

## Environment variables

<details>
<summary>Command and env reference</summary>

| Variable | Purpose |
|---|---|
| `CONTROL_PLANE_DB` | SQLite path (or `control-plane serve --db`) |
| `CONTROL_PLANE_URL` | Sidecar base URL (`http://127.0.0.1:8800`) |
| `CONTROL_PLANE_API_KEYS` | Seed keys: `name:tenant:scope+scope` |
| `CONTROL_PLANE_API_KEY` | Bearer the sidecar sends |
| `CONTROL_PLANE_CONFIG` | Governance YAML seed (else packaged `default.yaml`) |
| `TOKENOPS_URL` | TokenOps alias for the same origin |
| `TOKENOPS_API_KEY` | TokenOps alias for the Bearer |
| `TOKENOPS_EMBEDDED` | Must be **unset** when using this plane |

```
control-plane serve [--host 127.0.0.1] [--port 8800] [--db PATH] [--reload]
```

</details>

## Roadmap

The plane is early (0.x). Near-term:

- Harden SQLite under concurrent sidecar writes (`busy_timeout`, WAL discipline).
- Admin: persist the browser key so a refresh does not 401.
- Keep the HTTP contract stable enough for Chronicle 0.4 and TokenOps 0.2.

Ideas welcome via GitHub issues.

## Compatibility

| agentplane-control-plane | tokenops | agent-chronicle | notes |
|---|---|---|---|
| 0.1.x | ≤ 0.2.1 | ≥ 0.3.0 | single-op `/v1/ledger/*`, `PUT /v1/run-records` |
| **0.2.x** | ≤ 0.2.1 **and** `<next>` | ≥ 0.3.0 | **additive** — old clients keep working; adds `precheck` / `events:batch`, `run_state`, `data_scope` |
| [0.3.x](https://github.com/theagentplane/control-plane/issues/11) | `<next>`+ only | ≥ 0.3.0 | breaking — drops `run_registrations`, `PUT /v1/run-records`, legacy ledger wrappers |

### Breaking changes

- **0.2.0 → 0.3.0:** `run_registrations` folded into `runs` and dropped;
  `PUT /v1/run-records` (`create_run`) removed; `PATCH /v1/run-records` rejects
  `steps` / `cost_micros` (0.2.x only ignores them); legacy `/v1/ledger/halt/*` and
  single-op `/v1/ledger/{spent,inflight}/*` writes removed. Runs as an automatic
  `PRAGMA user_version` v3 migration. Released only after `tokenops <next>` stops
  calling `create_run`.

Full contract: [`docs/api-contract.md`](docs/api-contract.md).

## Documentation

- [API contract](docs/api-contract.md) — the TokenOps ⇄ control-plane wire spec
- [Design](docs/DESIGN.md) — storage, callers, scopes, keys
- [Releasing](RELEASING.md) — Trusted Publishing to PyPI
- [Changelog](CHANGELOG.md)
- [Chronicle](https://github.com/theagentplane/chronicle) · [TokenOps](https://github.com/theagentplane/tokenops)

## Contributing

Issues and PRs are welcome.

```bash
pip install -e ".[dev]"
ruff check control_plane tests
pytest -v
```

## Contributors

Thanks to everyone who has contributed.

[![Contributors](https://contrib.rocks/image?repo=theagentplane/control-plane)](https://github.com/theagentplane/control-plane/graphs/contributors)

---

If this is the missing box between your agents and the database, please [⭐ star the repo](https://github.com/theagentplane/control-plane) so more people can find it.

<div align="center">

Built by Susheem Koul and Tisha Chawla

</div>
