Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.git
.github
.pytest_cache
.ruff_cache
__pycache__
*.pyc
.venv
venv
*.egg-info
build
dist

# Not needed to build/run the image
tests
docs
ui
CHANGELOG.md
RELEASING.md
docker-compose*.yml
Dockerfile
.dockerignore

# Local dev artifacts — never bake a DB into the image
*.db
*.db-*
control_plane.db
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,59 @@

## [Unreleased]

### Added

- `control-plane start` / `stop` / `status` — run the plane detached from the
terminal as one managed background instance (PID + state file under a
`platformdirs` user-state dir; logs redirected to a file since the process is no
longer attached to a console). `start` waits for `/health` before returning and
reaps the process if it never comes up; `stop` escalates `terminate()` → `kill()`
on a timeout; both are idempotent (safe to call when already in the target state).
Cross-platform: POSIX detaches via `start_new_session`, Windows via
`DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP` — Windows has no SIGTERM, so `stop`
there is a hard stop, not a graceful drain.
New deps: `psutil`, `platformdirs`.
- `Dockerfile` + `docker-compose.yml` — the repo's first container image. Builds from
source (`pip install .`), runs as a non-root user, SQLite on a `/data` volume,
built-in `HEALTHCHECK` against `/health`. Entrypoint is `control-plane serve`
(foreground) — the container is its own process supervisor, so the new
`start`/`stop`/`status` background mode is for local dev only, not for images.

## [0.2.0] — TokenOps remote-only (#10)

**Additive on the wire and schema.** 0.1-era clients keep working against 0.2.x; the
breaking fold (drop `run_registrations`, remove `create_run`, narrow `PATCH`) is
deferred to 0.3.0 (#11), released after `tokenops <next>`.

Added
- Wire contract: `docs/api-contract.md`.
- `POST /v1/ledger/events:batch` — batched ledger writes (`spent_add` with multi-target
fan-out, `admit`/`complete`, `step`, `halt_mark`/`halt_clear`), applied in array order
in one transaction, idempotent per `idempotency_key` (`ledger_events` table).
- `POST /v1/ledger/precheck` — one read for a pre_call pass: halt + requested
spent / inflight / window slices (`SqliteStore.precheck`).
- `run_state` table — per-run `step_count` / bounded window / velocity, fed by `step`
events; `SqliteStore.get_run_state`.
- `policy_instances.data_scope` (`local` | `global`, default `local`) — surfaced in
`GET /v1/governance/{agent}`, `PUT /v1/policies`, and YAML seed.
- `GET /v1/ledger/runs/{run_id}/halt` (GET / POST / DELETE) — run-scoped halt;
`/v1/ledger/halt/*` kept as aliases.
- `PRAGMA user_version` schema versioning (`SqliteStore.SCHEMA_VERSION = 2`) +
forward-only `_apply_migrations()`. Opening a 0.1 DB adds the new tables/columns and
bumps the version — no data loss. The 0.3.0 destructive fold (#11) slots in as v3.

Changed
- `POST /v1/runs` response includes `registered_at`; `register_run` returns the
persisted row so the SDK can bind without a follow-up `GET .../registration`.
- `GET /health` advertises `max_batch` / `max_body_bytes`.
- Version → `0.2.0`.

Deprecated (removed in 0.3.0)
- `PATCH /v1/run-records` — client-sent `steps` / `cost_micros` are now **ignored**
(logged); they are derived from `run_state` / `ledger_spent`.
- `PUT /v1/run-records` (`create_run`), the `run_registrations` table, and the legacy
`/v1/ledger/halt/*` + single-op `/v1/ledger/{spent,inflight}/*` writes.

## [0.1.0] — 2026-08-14

- First PyPI release of the shared AgentPlane control plane.
Expand Down
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1
FROM python:3.12-slim

LABEL org.opencontainers.image.title="agentplane-control-plane"
LABEL org.opencontainers.image.description="Shared AgentPlane control plane: Chronicle envelopes + TokenOps governance over HTTP"
LABEL org.opencontainers.image.source="https://github.com/theagentplane/control-plane"

WORKDIR /app

# Only what pyproject.toml + hatchling need to build the wheel (see
# [tool.hatch.build.targets.wheel] / force-include) — keeps the build context and
# image small, and cache-friendly (dependency layer doesn't invalidate on doc edits).
COPY pyproject.toml README.md LICENSE ./
COPY config ./config
COPY control_plane ./control_plane

RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir .

# SQLite lives on a volume so it survives `docker compose down` / image upgrades.
ENV CONTROL_PLANE_DB=/data/control_plane.db
VOLUME ["/data"]

# Least privilege: the process only ever needs to read/write /data.
RUN useradd --create-home --uid 1000 controlplane \
&& mkdir -p /data \
&& chown -R controlplane:controlplane /data
USER controlplane

EXPOSE 8800

HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8800/health', timeout=2)" || exit 1

# Foreground, PID 1 — the container is the process supervisor. `control-plane start`
# (the background/detached mode for local dev) must not be used here: if PID 1 exits
# right after spawning a detached child, the container exits with it.
CMD ["control-plane", "serve", "--host", "0.0.0.0", "--port", "8800"]
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,55 @@ pip install -e ".[dev]"
## Quick start

```bash
control-plane serve --port 8800 --db control_plane.db
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.
Expand Down Expand Up @@ -197,8 +239,28 @@ The plane is early (0.x). Near-term:

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)
Expand Down
Loading
Loading