Pi Managed Backend
July 27, 2026 · View on GitHub
A self-deployable service that gives the Pi coding agent managed-agent capabilities: remote agent sessions, scheduled jobs (crons), durable state, sandboxed execution via microsandbox microVMs, and a Pi client extension that makes it all feel native to a local Pi user.
The service is tailored for Pi — it adapts the concepts of managed agents (agents, sessions, environments, events, tools, memory, multi-agent orchestration, outcomes, scheduled deployments) to Pi-native idioms rather than mirroring any other API.
- Getting started:
docs/getting-started.md(install → running, easy steps) - Architecture:
docs/architecture.md(layered overview — start here) - API reference:
docs/api-reference.md(wire contract) - Deployment:
docs/deploy.md(full deploy guide) - Operations:
docs/operations.md(run it in production — backup, restore, upgrade, incidents) - Web console:
docs/console.md(design),docs/console-spec.md(normative spec),docs/console-user-journeys.md(journeys)
Quick start (dev)
New here?
docs/getting-started.mdis the same path as a friendly step-by-step (with the macOS / no-KVM route and the vault-key step spelled out). The condensed version follows.
Prerequisites
- Node.js ≥ 22.19 (the embedded Pi agent's
undicirequires it) - pnpm (
corepack enable— the version is pinned viapackageManagerinpackage.json) - Docker (for the dev Postgres; the object store defaults to the local filesystem)
- Linux with
/dev/kvm(for real microVM sandboxes — macOS works for everything except sandbox execution)
1. Install
git clone <repo> pi-backend && cd pi-backend
pnpm install
2. Start Postgres
docker compose up -d postgres
The object store defaults to the local filesystem (OBJECT_STORE_ROOT, default
./data/objectstore) — nothing else to start. The compose file's MinIO service is
only for the S3 adapter's contract test, not for running the backend.
3. One-time microsandbox bootstrap (for real sandboxes)
node -e "import('microsandbox').then(m => m.install())"
This populates ~/.microsandbox/ with the libkrunfw kernel + agentd. Skip if you only need the API without sandbox execution (SANDBOX_RUNTIME=disabled).
4. Build
pnpm build
5. Boot the backend
DB_URL=postgres://pi:pi@localhost:5432/pi \
OBJECT_STORE_ROOT=./data/objectstore \
SANDBOX_RUNTIME=enabled \
PORT=3000 \
node --enable-source-maps packages/backend/dist/main.js
On boot the backend runs migrations (forward-only), wires all subsystems, and binds 0.0.0.0:3000.
6. Verify
# Liveness (always 200 if the process is up)
curl http://localhost:3000/healthz
# Readiness (200 when db + objectStore + sandbox are all up)
curl http://localhost:3000/readyz
7. Create your first tenant + API key
# Onboarding (creates a tenant + admin API key, returns install instructions)
curl -X POST http://localhost:3000/v1/onboarding/signup \
-H 'content-type: application/json' \
-d '{"tenantName":"my-org","adminEmail":"me@example.com"}'
Use the returned apiKey for all authenticated calls:
# Create an agent
curl -X POST http://localhost:3000/v1/agents \
-H 'authorization: Bearer pmb_live_...' \
-H 'idempotency-key: agent-1' \
-H 'content-type: application/json' \
-d '{"name":"coder","model":{"provider":"openai","id":"gpt-4o"}}'
# Create an environment
curl -X POST http://localhost:3000/v1/environments \
-H 'authorization: Bearer pmb_live_...' \
-H 'idempotency-key: env-1' \
-H 'content-type: application/json' \
-d '{"name":"python","type":"cloud","image":"ubuntu:22.04","resources":{"cpus":2,"memoryMiB":2048},"networking":{"mode":"unrestricted"}}'
# Create a session
curl -X POST http://localhost:3000/v1/sessions \
-H 'authorization: Bearer pmb_live_...' \
-H 'idempotency-key: sess-1' \
-H 'content-type: application/json' \
-d '{"agent":"agent_...","environmentId":"env_..."}'
# Send a message
curl -X POST http://localhost:3000/v1/sessions/sess_.../events \
-H 'authorization: Bearer pmb_live_...' \
-H 'idempotency-key: msg-1' \
-H 'content-type: application/json' \
-d '{"type":"user.message","content":"write hello world to /mnt/session/outputs/hello.txt"}'
# Stream events (SSE)
curl -N http://localhost:3000/v1/sessions/sess_.../stream \
-H 'authorization: Bearer pmb_live_...'
8. Install the client extension in local Pi
pi install npm:@pi-managed/client
# or add to .pi/settings.json extensions array
Then in Pi:
/remote:config # point at your backend + paste the API key
/remote:delegate "run the test suite and report failures"
Repository layout
packages/
backend # the service (Fastify + Postgres + microsandbox)
client-extension # @pi-managed/client — the Pi extension (§24)
contracts # zod schemas + TS types mirroring api-reference.md
testkit # shared test fixtures (fakes, conformance kits)
worker # default self-hosted worker (§10.4)
web-console # web console UI (§26.6, console spec)
docs/
getting-started.md # install → running (dev / local, easy steps)
architecture.md # layered architecture overview
user-journeys.md # persona journeys (platform admin / tenant admin / user)
api-reference.md # the wire contract
db-schema.md # Postgres schema (generated — see `pnpm db:schema:gen`)
deploy.md # deployment guide
operations.md # production runbook (backup, restore, upgrade, incidents)
session-worker-pool.md # harness-isolation pool (`SESSION_WORKER_MODE=pool`)
...
Development
pnpm install # install deps
pnpm build # build all packages
pnpm test # run all tests
pnpm lint # eslint
pnpm typecheck # tsc --noEmit
Tests use vitest; integration tests use testcontainers (real Postgres, auto-detecting docker or a rootless podman socket). @kvm-tagged tests need /dev/kvm + an installed microsandbox runtime. Locally, a missing capability skips cleanly with a loud stderr banner; in CI, PI_REQUIRE_INTEGRATION makes the same gap a hard failure instead — see docs/deploy.md §9.
Configuration
All config is via environment variables (env > config file > defaults). Boot is fail-closed: an invalid or missing required value (e.g. no vault key) prints an error and exits 1 rather than starting in a half-configured state. Key vars:
| Env var | Required | Default | Meaning |
|---|---|---|---|
DB_URL | yes | — | Postgres connection URL |
OBJECT_STORE_KIND | no | filesystem | filesystem → a local directory at OBJECT_STORE_ROOT; gcs → Google Cloud Storage on GCS_BUCKET. S3 stays a composition-time injection |
OBJECT_STORE_ROOT | no | ./data/objectstore | Filesystem root, used when OBJECT_STORE_KIND=filesystem. Holds the JSONL transcripts — put it on durable storage |
GCS_BUCKET | yes* | — | GCS bucket. Required when OBJECT_STORE_KIND=gcs (boot fails closed without it). Credentials via Application Default Credentials — no key material in backend env |
PI_SESSION_LOCAL_DIR | no | ./data/sessions | Durable host-side root for per-session JSONL logs. Deliberately not /tmp — this is the file the object-store sync and cold-wake restore depend on surviving a host reboot |
SANDBOX_RUNTIME | no | disabled | enabled to wire real microVMs |
SANDBOX_MODE | no | single | single → one host-local MicrosandboxProvider; multi → routes across a SANDBOX_HOSTS pool via MultiHostSandboxProvider. Only meaningful with SANDBOX_RUNTIME=enabled |
SESSION_WORKER_MODE | no | inproc | inproc → every session harness runs in the control-plane process (default). pool → harnesses run in bounded child processes sharded by session id, bounding the blast radius of one session's crash/leak. See docs/session-worker-pool.md |
RATE_LIMIT_RPM / RATE_LIMIT_ANON_RPM | no | 600 / 30 | Per-tenant and per-IP (unauthenticated paths, e.g. signup) request-per-minute ceilings |
RATE_LIMIT_STORE | no | memory | memory (per-process — N replicas allow N× the ceiling) or postgres (shared, global ceiling) |
PORT | no | 3000 | HTTP bind port |
VAULT_KEY / VAULT_KEY_FILE | yes | — | 32-byte key (hex/base64) or key-file path for vault-secret encryption. Boot refuses to start without one (no NODE_ENV=test implicit opt-in — the explicit ALLOW_EPHEMERAL_VAULT_KEY flag is the only escape hatch). Deprecated aliases: MSB_SECRET_ENCRYPTION_KEY / MSB_SECRET_ENCRYPTION_KEY_FILE |
ALLOW_EPHEMERAL_VAULT_KEY | no | false | Dev-only escape hatch: boot with a throwaway key when no VAULT_KEY is set. Never set in production — stored secrets become undecryptable after a restart |
PI_REQUIRE_INTEGRATION | no | unset | Test-time only (not a server boot var). containers / kvm / 1 makes a missing container runtime or /dev/kvm a hard test failure instead of a silent skip — see docs/deploy.md §9 |
See docs/deploy.md for the full list + deployment shapes.
Model-provider keys
Per-agent model-provider API keys are not environment
variables and are never read from the backend process's own env for a tenant session.
Each key is stored as a model_provider_key-category credential in a tenant's vault
(AES-256-GCM at rest, decrypted host-side only), keyed by Pi provider id (openai,
etc.), and resolved fresh at every session wake. A session whose model has no resolved
key fails closed — session construction throws before any model call, so it can never
silently fall through to a provider API key in the host's environment and bill the host.
API-key scopes
API keys carry a scopes array enforced on every route: admin (wildcard — satisfies
everything), read / write (checked per-method — GET/HEAD need read, everything
else needs write), and self_hosted_worker:<envId> (satisfies nothing but its own 3
work-queue routes — a worker key is deny-by-default everywhere else). New keys default to
least-privilege, not admin.
Documentation
- Getting started — the shortest path from a fresh clone to a running backend (dev / local, with the macOS / no-KVM route)
- Architecture — layered overview: executive summary, the whole system at a glance, containers, backend internals, runtime views, cross-cutting concerns, decision log
- User journeys — platform admin, tenant admin, and user (Pi coder) journeys end to end, plus machine actors
- Deploy guide — prerequisites, config, boot, health, graceful shutdown, deployment shapes
- Operations — the production runbook: upgrade/restart, backup, restore/DR, key rotation, fronting with TLS, incident response
- API reference — every endpoint, request/response schema, error taxonomy, event catalog, SSE wire format
- DB schema — every Postgres table, indexes, constraints, encrypted-column strategy
- Internal contracts — the port interfaces (SandboxProvider, SessionRuntime, SecretStore, …)
- Plugin authoring — how to write custom sandbox/secret/scheduler providers
- Observability — OTEL span/metric conventions, msb-metrics pipeline, Grafana dashboards
- Session-worker pool —
SESSION_WORKER_MODE=poolharness isolation
License
(See LICENSE — TBD)