pi README

The project's README from github.com/15838328874/pi. As one markdown file: README.md.

pi-py

English | 中文

ci · license: MIT

实测战绩:真实模型企业测评 5/5 ✅ · 安全回归 40/40 ✅ · SandboxFS 组合 11/11 ✅ · Python 3.12 · CubeSandbox 会话级沙箱

A self-hostable, extensible AI agent platform: sandboxed tool execution, multi-tenant governance (JWT/quotas/rate limits/audit), a canonical trajectory log, an eval harness with scoring and A/B, an RL data flywheel (rollout→reward→SFT/RLVR JSONL), and protocol-level tool extensibility via MCP + Skills. Coding is the first fully-developed scenario, not the boundary. Python reimplementation of the pi coding agent harness, shipped as a multi-user HTTP service: streaming LLM layer, asyncio agent loop, coding tools, and sandboxing (CubeSandbox microVMs in production, Docker locally). Mirrors pi's package layering:

pi (TypeScript) pi-py (Python)
pi-ai pi.llm (openai / anthropic / fake)
pi-agent-core pi.agent (asyncio loop + events)
pi-coding-agent pi.tools + pi.prompt
daemon / server pi.server (FastAPI + SSE), pi.cli (serve/migrate)

架构总览

flowchart LR
    subgraph 入口
        CLI[cli.py]
        API[app.py FastAPI]
    end
    subgraph 编排
        RM[runner.py RunManager<br/>锁 / 背压 / 超时 / SSE]
    end
    subgraph 智能体核心
        LOOP[AgentLoop loop.py]
        COMP[compaction.py]
    end
    subgraph 能力层
        TOOLS[工具层 tools/]
        LLM[llm/ provider + 降级链]
    end
    subgraph 沙箱与存储
        SBX[沙箱 CubeSandbox VM / Docker]
        DB[(MySQL)]
        REDIS[(Redis)]
        MILVUS[(Milvus)]
    end
    API --> RM --> LOOP
    LOOP --> LLM
    LOOP --> TOOLS --> SBX
    TOOLS --> MILVUS
    RM --> DB
    RM --> REDIS

完整架构、Agent Loop 状态机、Memory Judge 决策树见 ARCHITECTURE.md §3。

Highlights

精选亮点(完整叙事、设计取舍与踩坑实录都在 ARCHITECTURE.md):

亮点 一句话 详情
🏜️ 会话级沙箱 懒加载 + 会话级复用池:纯聊天回合零 VM,首次 bash 才现场建 VM(毫秒级冷启),跨回合延续状态,空闲超时 / 内存压力自适应回收;崩溃隔离、内存模型 = 并发活跃回合 × 256Mi 而非会话数 架构总览 · 设计笔记
🛡️ SSRF 防线 进程内抓取工具(web_fetch/web_search)已移除,抓取一律降级到沙箱内执行——宿主内网(MySQL/Redis/云元数据)对模型不可达 坑 20(已解决)
🗂️ 工作区闭环归档 每回合基线快照 → 结束 tar.gz + 差异元数据(added/modified/deleted),MinIO 惰性接口就绪 实测数据
🧪 自治验证 真实模型跑企业任务:数据分析/日志解析/GitHub 情报/文档摘要/数据清洗 5/5 PASS;安全回归 40/40;沙箱组合 11/11 测试体系
☁️ 生产就绪 部署手册(10 节)+ 生产就绪审计(7 项修复、剩余清单全部闭环) 部署手册 · 审计
🔐 多租户治理 JWT / 配额 / 限流 / 审计 / 会话级分布式锁(同会话并发直接拒绝,跨会话独立沙箱并行) 架构总览 · 架构手册

文档地图(三个文档各管一段,知识点不重复):

文档 定位 什么问题看它
README.md 门面 这是什么、怎么装、怎么跑(快速上手入口)
ARCHITECTURE.md 技术手册 + 叙事 每个模块每个函数、配置全表(§13)、坑清单(§17)、差距清单(§19);设计取舍、测试样例、术语表、实测数据
ROADMAP.md 状态与路线图 什么做完了、什么没做、下一步做什么(含环境区分表)
docs/adr.md 设计取舍索引 关键决策一页扫读:每条「选了 A · 没选 B · 为什么 · 代价」,收敛自各设计笔记
docs/(三件) CubeSandbox 专项 沙箱设计笔记 / 生产部署手册 / 生产就绪审计——专项文档,不重复核心文档内容

推荐阅读路径:先看本页 ## Highlights 建立全局印象 → 想深入了解设计取舍、踩坑故事、实测细节 → 直接读 ARCHITECTURE.md(技术手册 + 叙事);对照代码逐模块也看它;做没做、下一步 → ROADMAP.md。

Linux only. The former single-user CLI/TUI mode, the local SQLite session store, and all Windows/WSL support have been removed — this package is the service and nothing else.

Install

cd pi-python
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[production]"   # deployable server: asyncpg + aiomysql + redis + alembic + otel
pip install -e ".[dev]"          # test suite only: pytest + aiosqlite

Configure

Environment variables, or a .env file (project root ./.env, or ~/.pi-py/.env; the first file found wins, and existing environment variables always override it):

PI_DATABASE_URL=mysql+aiomysql://user:pass@host:3306/pi_py   # required
PI_REDIS_URL=redis://user:pass@host:6379/0                   # locks / limits / revocation
PI_REDIS_NS=prod                                             # key namespace, isolates environments
PI_JWT_SECRET=<openssl rand -hex 32>                         # required in production
PI_MODEL=openai/qwen3.8-max
OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://your-openai-compatible-endpoint/v1
# optional vector semantic memory: set ALL FOUR to enable (else lexical retrieval)
PI_EMBEDDING_URL=https://.../compatible-mode/v1/embeddings
PI_EMBEDDING_API_KEY=sk-...
PI_EMBEDDING_MODEL=qwen3.7-text-embedding
PI_MILVUS_URI=http://milvus-host:19530
# optional extra tool sources (empty = off)
PI_MCP_SERVERS='[{"name":"filesystem","command":["npx","-y","@modelcontextprotocol/server-filesystem","/ws"]}]'
PI_SKILLS_DIR=/opt/pi-py/skills

Notes:

Run

pi-py migrate                                  # alembic upgrade head against PI_DATABASE_URL
pi-py serve --host 0.0.0.0 --port 8300

本地测试环境(MySQL + Redis + Milvus 全套 Docker 化 + 真实模型,与生产严格区分): 见 deploy/local-dev.md,基础设施编排在 deploy/docker-compose.local.yml, 环境变量模板在 deploy/env.local.example(生产是云端托管 MySQL/Redis/Milvus + docker-compose.cloud.yml)。

Registration is open by default (turn it off with PI_ALLOW_REGISTER=0 for public demos): every signup is a normal user. Admin is granted only by writing the database directly — there is no route for it:

UPDATE users SET is_admin=1 WHERE username='alice';

Because signup is unauthenticated and unlimited, keep the port off the public internet (security group / firewall). Exposing it means anyone can create accounts, each carrying a monthly token quota and a workspace directory.

curl -X POST :8300/v1/auth/register -d '{"username":"alice","password":"..."}'
TOKEN=$(curl -s -X POST :8300/v1/auth/login -d '{"username":"alice","password":"..."}' | jq -r .access_token)
SID=$(curl -s -X POST :8300/v1/sessions -H "Authorization: Bearer $TOKEN" -d '{"title":"t"}' | jq -r .id)
curl -N -X POST :8300/v1/sessions/$SID/runs -H "Authorization: Bearer $TOKEN" -d '{"prompt":"hi"}'

Tools

Twelve built-ins, registered in tools/__init__.py::all_tools():

bash (timeout + exit code), read (line numbers, paging), write, edit (exact unique match, replace_all, unified diff), grep (regex, skips VCS/build dirs), find (glob), ls, remember, recall, spawn_subagents, list_files, fetch_file (12 tools total; in-process web_fetch/web_search were removed over SSRF risk — sandboxed bash is the fetch path).

Three extra tool sources, merged by ToolRegistry (builtin first; name collisions keep the builtin; one broken source never takes the others down):

Context compaction is automatic once history exceeds 80,000 chars: an LLM-written summary replaces the old prefix and the most recent 8 messages are kept verbatim, in memory for the current run. The summary is also persisted to the compactions table (via on_compact, fail-soft) while raw messages stay append-only, and each subsequent turn loads [latest summary] + [messages after it] instead of re-summarizing — the same history pays the summarization LLM call only once.

Enterprise security

PI_POLICY points at a JSON gate applied to every tool call. The policy.json in the repo root is the live policy for both deployment modes (bare metal points PI_POLICY straight at it; both compose files bind-mount the same file to /etc/pi-py/policy.json). Shape:

{
  "deny_command_patterns": [
    "(?:^|[;&|(]\\s*)sudo\\b",
    "\\brm\\s+(-{1,2}[a-z-]+\\s+)*/(\\s|$|\\*)"
  ],
  "path_sandbox": true,
  "redact": true
}

Capability vocabulary (declared in tools/*.py): filesystem.read (read/ls/grep/find, list_files), filesystem.write (write/edit, fetch_file), process.execute + both filesystem caps (bash), memory.read (recall), memory.write (remember), agent.delegate (spawn_subagents), knowledge.retrieve (rag_search, 只读检索本用户 已入库文档). Check order: deny_tools → deny_capabilities → allow_capabilities → bash patterns → path sandbox.

Patterns are anchored to command position ((?:^|[;&|(]\s*)) on purpose: a bare "sudo" also blocks grep -rn 'sudo' src/, and a false positive is harder to diagnose than a miss because it silently breaks ordinary agent work. tests/test_security.py::TestShippedPolicy pins both directions against the shipped file — 22 commands that must be denied, 15 that must stay allowed — so add cases to those lists before touching a regex.

A policy file can only add rules: server_policy() forces path_sandbox and redact on even if the file omits them or sets them to false (Policy.from_dict defaults both to False, so a deny-list-only file would otherwise switch off the workspace sandbox and redaction). Without any policy file the server still runs with path_sandbox + redact on. Every tool call (allowed and denied) is appended to the audit log ~/.pi-py/audit.jsonl, rotated daily as audit-YYYY-MM-DD.jsonl (JSONL: timestamp, user, session, tool, args, decision, outcome).

Multi-user server

Concern Implementation
Auth JWT (HS256, PyJWT), PBKDF2 password hashing (200k iters) offloaded via asyncio.to_thread so concurrent logins cannot freeze the event loop; open unauthenticated signup, every account is a normal user — admin is granted only by UPDATE users SET is_admin=1 ...
Session isolation sessions/messages scoped per user; cross-user access returns 404 (no existence leak)
Concurrency per-session lock (1 running turn per session) + global semaphore (PI_MAX_CONCURRENT_RUNS, default 8) + run timeout (PI_RUN_TIMEOUT_SECONDS, default 600)
Rate limiting fixed-window per user (PI_RATE_LIMIT_RUNS_PER_MIN, default 20), 429 + Retry-After
Audit append-only daily-rotated JSONL: tool calls, policy decisions, compaction, and every register/login attempt with client IP + user agent (never the password). All fields truncated, because the failed-login path is attacker-controlled and LoginIn.username is unbounded. Now that it holds IPs it is personal data — give it a retention period
Reverse proxy PI_FORWARDED_ALLOW_IPS (default 127.0.0.1) lists whose X-Forwarded-For to trust; both compose files set it to 172.16.0.0/12 so the Caddy container qualifies. A wrong value fails silently — every client is logged as the proxy, and any IP-keyed limit collapses into one bucket. Never *: uvicorn then returns the leftmost, client-supplied entry
Streaming SSE (text/event-stream): start / text_delta / toolcall_start / toolcall_end / compaction / turn_end / error / done
Storage SQLAlchemy 2.0 async over MySQL (aiomysql) or PostgreSQL (asyncpg); PI_DATABASE_URL required, schema managed by Alembic. Raw run trajectories append to PI_TRAJECTORY_PATH (plus the runs table for structured lookup). Session workspaces archive at turn end as tar.gz + diff metadata (PI_ARCHIVE_DIR, optional MinIO via PI_ARCHIVE_S3_*, PI_ARCHIVE=0 off) (default ~/.pi-py/trajectories.jsonl, daily rotation; "" = off) — persistence failures are logged, never allowed to fail a run
Workspaces per-user sandbox dir PI_WORKSPACE_ROOT/<user>/
Observability JSON access log with request id + latency; /healthz, /readyz; full audit trail

API (see /docs for OpenAPI): POST /v1/auth/register|login|logout, GET /v1/me, GET/POST /v1/sessions, GET /v1/sessions/{id}, DELETE /v1/sessions/{id}, GET /v1/sessions/{id}/messages, GET /v1/sessions/{id}/trajectory (latest raw run, ownership-checked), GET /v1/trajectory/{run_id} (replay one run) and GET /v1/admin/trajectory/{run_id} (admin cross-user replay), POST /v1/sessions/{id}/runs (SSE), GET /v1/usage, GET /v1/admin/users, PATCH /v1/admin/users/{u}, POST /v1/admin/users/{u}/revoke (open registration off via PI_ALLOW_REGISTER=0), GET /v1/admin/audit (?day=YYYY-MM-DD&user=&tool=&event=, DB-backed with jsonl fallback), GET /v1/admin/stats (today's aggregates + fleet sizes), GET /v1/admin/usage (monthly per-user), POST/GET/DELETE /v1/files + /v1/files/commit + /v1/files/{id}/url (MinIO presigned file pipeline, sha256 user-scoped dedup). Official async SDK: from pi.client import PiClient (auth/sessions/messages/trajectories/usage + SSE run stream, trust_env=False). Zero-build web UI (single-file vanilla JS pages): user app at GET /ui/app.html (assistant markdown rendering, model picker per session, thinking-process display, file upload/download panel, register tab hidable via PI_ALLOW_REGISTER=0). (login/sessions/chat with full tool-call trace + monthly usage), the trajectory viewer at GET /ui/trajectory.html?session=<id>, and the admin console at GET /ui/admin.html (overview/users+quota/audit).

Observability, metering, resilience

Concern Implementation
Tracing PI_TRACER=jsonl (default, built-in span file) or otel (OpenTelemetry bridge, install pi-py[observability]); spans: agent.run / llm.call / tool.call with duration + status
Prometheus metrics GET /metrics (text format 0.0.4, prometheus-client via pi-py[observability]): runs/llm/tool/memory/HTTP RED counters + histograms + pi_runs_in_flight gauge; degradation counters for lexical memory fallback and model-chain fallbacks, plus sandbox health series (pi_sandbox_create_failures_total / pi_sandbox_command_timeouts_total / pi_sandbox_close_failures_total / pi_sandbox_create_duration_seconds). Every family projects from one existing recording point (tracer span close / trajectory / RunManager / middleware) - labels stay bounded (model/tool/status/route, never user or session). Gate with PI_METRICS_TOKEN (wrong token -> 404); PI_METRICS=0 disables. Scrape config: deploy/prometheus.yml
Cost accounting every completed run records tokens + est. cost (per-model price table, PI_PRICES_FILE override); GET /v1/usage returns monthly per-model breakdown
Quotas per-user monthly token quota (PI_DEFAULT_QUOTA_TOKENS, default 1M); exhausted -> HTTP 402
Model fallback PI_FALLBACK_CHAIN="openai/qwen3.8-max,openai/qwen3.8-flash,openai/qwen3.6-flash"; transient errors (connection/timeout/429/5xx) retry with exponential backoff (2x, 0.5s base), then degrade; mid-stream failures never replay; non-transient errors propagate
Resilience metering/audit/callback failures are logged, never allowed to fail a run

Deployment hardening: Redis, sandbox, Docker

Concern Implementation
Distributed rate limit + session locks PI_REDIS_URL=redis://... switches both to Redis (INCR+EXPIRE window / SET NX EX lock); without it they degrade to process-local, which is only correct for a single instance
Token revocation POST /v1/auth/logout blacklists the token's jti for its remaining life; POST /v1/admin/users/{u}/revoke bumps a per-user epoch invalidating all their tokens (1s conservative window); disabled accounts are rejected at login and on every request
MySQL PI_DATABASE_URL=mysql+aiomysql://user:pass@host:3306/pi_py (install pi-py[mysql]) — the engine adds charset=utf8mb4 + connection recycling automatically
PostgreSQL PI_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/pi (install pi-py[postgres]) — same schema and Alembic path
Tool sandbox PI_SANDBOX=docker runs bash commands in containers (warm pool, --network none, cgroup limits). PI_SANDBOX=cubesandbox runs in CubeSandbox microVMs via the E2B-compatible SDK (PI_CUBE_API_KEY, host KVM/nested virtualization required) — session workspace lives inside the VM, commands wrapped in GNU timeout (124 → timed_out, zero residue), non-zero exit codes pass through, >10MB workspace load rejected with an actionable error. VM lifecycle: lazy creation (chat turns run with zero VMs), a reuse pool per session (pool_hits), LRU + idle-TTL + host-memory-pressure adaptive eviction (PI_SANDBOX_POOL_*). Any other value fails at startup — it used to fall through to running commands inside the app process, where they inherit PI_JWT_SECRET and PI_DATABASE_URL. Design/runbook: docs/cube-sandbox-design-notes.md / docs/production-deployment.md
Warm container pool docker mode defaults to a per-workspace container pool with lazy creation (the first bash call of a turn creates/reuses the container; chat-only turns never create one), commands run via docker exec (no per-call container lifecycle), idle entries are recycled (PI_SANDBOX_IDLE_TTL, default 600s), LRU-evicted at capacity (PI_SANDBOX_POOL_MAX, default 16) with soft overshoot when all are busy, and vanished containers are rebuilt transparently. PI_SANDBOX_POOL=0 restores the legacy fresh-container-per-call behaviour. Warm containers self-terminate after PI_SANDBOX_WARM_LIFETIME (default 2h) so a crashed app cannot orphan them forever
Container resource limits PI_SANDBOX_MEMORY (1g) / PI_SANDBOX_PIDS (256) / PI_SANDBOX_CPUS (1.0). Docker's own defaults are no limit at all (Memory=0, NanoCpus=0, no PidsLimit), and registration is open, so uncapped containers let any account exhaust the host with one command — --network none does not cover resource exhaustion. Applied at all four container-creation paths (cold CLI, cold Engine API, warm CLI, warm API); --memory-swap is set equal to --memory because docker otherwise allows 2x via swap. Keep PI_SANDBOX_MEMORY × PI_MAX_CONCURRENT_RUNS well under physical RAM (shipped: 1g × 8 = 8 GiB of 16 GiB). PI_SANDBOX_USER defaults to the app's own uid:gid so files written through the bind mount stay readable and deletable by it — 0:0 bare metal, 10001:10001 under compose, no config change needed
Fail-loud config validation Two settings whose wrong value silently removed isolation now refuse to start or self-correct: an unrecognised PI_SANDBOX raises in create_app (and the legitimate local path logs a warning spelling out the consequence), and server_policy() forces path_sandbox/redact on so a PI_POLICY file can add rules but never subtract them
Container image multi-stage Dockerfile (non-root uid 10001, HEALTHCHECK /healthz, migrations baked into /opt/pi-py); docker-compose.local.yml (MySQL+Redis+Milvus 基础设施) 与 docker-compose.cloud.yml(生产 app+caddy,DB 云端托管)
Load tested (historical, inherited) tools/loadtest.py: previous maintainer's run — 50 users x 2 rounds on self-hosted PG+Redis, 100/100 success, p95 ~2.4s, p99 ~3.1s (includes PBKDF2 registration); same-session concurrency correctly serialized by the lock. Not re-measured on the managed MySQL+Redis deployment. The tool prints percentiles to stdout and writes no result file
Sandbox capacity (measured on this deployment) tools/sandbox_bench.py, 4 vCPU / 16 GiB ECS, PI_SANDBOX=docker warm pool, python:3.12-slim, network off. Throughput saturates at ~52 exec/s by N=4 concurrent workspaces and stays flat to N=64 with host CPU at 96-100%, so p50 latency is pure queue depth: 44ms @ N=1, 154 @ 8, 292 @ 16, 567 @ 32, 1162 @ 64 — zero failures at every level. A heavier command (python -c) drops the ceiling to 42/s. Memory is not the constraint (26 MiB per warm user); cold-starting 64 containers at once takes 3.0s. The Engine API transport is only 1.3x faster per call than docker exec, so the ceiling is dockerd/runc, not the CLI transport. Note PI_SANDBOX_POOL_MAX=16 does not cap a simultaneous arrival storm — it logs "allowing temporary overshoot" and creates all 64. In the shipped config PI_MAX_CONCURRENT_RUNS=8 caps concurrent turns first, so the sandbox runs at ~154ms p50 with 6x headroom. Measured before the per-container ceilings existed — the host-CPU-saturation figures in particular predate PI_SANDBOX_CPUS=1.0, so re-run the bench if you tighten the caps
Multi-instance verified (historical, inherited) two instances sharing one Redis/DB: cross-instance session lock confirmed (Redis lock key observed with expected TTL), compose replicas knob added
TLS docker compose --profile tls up -d fronts the app with Caddy (automatic HTTPS, SSE-friendly); see deploy/Caddyfile
# full stack
docker compose up -d
docker compose exec app pi-py migrate

# or manual: you run redis + the database yourself
export PI_DATABASE_URL="postgresql+asyncpg://pi:pass@localhost:5432/pi"
export PI_REDIS_URL="redis://localhost:6379/0"
export PI_SANDBOX=docker
pi-py migrate && pi-py serve --host 0.0.0.0 --port 8300

Production extras in pyproject.toml: pip install pi-py[production] (asyncpg + aiomysql + redis + alembic + opentelemetry).

Cloud deployment (Volcano Engine)

App on a 4 vCPU / 16 GiB ECS, MySQL 8.0.43 + Redis 7.0.15 as managed instances reached over the VPC private network (no DB containers on the host):

cp deploy/env.cloud.example .env   # fill in MYSQL_PASSWORD / REDIS_PASSWORD / PI_JWT_SECRET / model keys
docker compose -f docker-compose.cloud.yml up -d --build

Layout

src/pi/
  models.py                 block-based message model (pydantic)
  prompt.py                 system prompt
  llm/                      base, registry, openai, anthropic, fake, fallback, think_filter
  agent/                    loop.py + events.py + compaction.py
  tools/                    base + bash/read/write/edit/grep/find/ls + files/memory/mcp/skill/subagent/rag + registry + sandbox
  security/                 policy.py + audit.py + redact.py
  observability/            tracing.py + metering.py + prices.py
  server/                   config, db, auth, cache, ratelimit, runner, app (FastAPI)
  cli.py                    argparse entrypoints: serve / migrate
migrations/                 Alembic versions (0001 schema, 0002 user_active)
tests/                      pytest suite: 529 tests against the real MySQL + Redis stack (LLM/embedding/Milvus use fakes)
tools/loadtest.py           SSE load test
tools/seed_testdb.py        seed a reusable *_test database (idempotent, refuses production)
tools/sandbox_bench.py      docker warm-pool capacity sweep (concurrent users -> exec latency)
deploy/                     Caddyfiles, cloud runbook, .env template

Testing

pip install -e ".[dev]"
python -m pytest -q          # 521 passed, 8 skipped

The suite runs against the real local stack (MySQL pi_py_test + Redis db1, same as production since 2026-09-27 - see deploy/docker-compose.local.yml or the CI services). tests/conftest.py pins PI_SANDBOX / PI_POLICY empty and PI_TRACER=noop before pi is imported, so a production .env in the repo root cannot leak into a test run. LLM / embedding / Milvus use fakes (FakeProvider et al.), a handful of tests use throwaway SQLite files directly, and async tests are wrapped in asyncio.run() (no pytest-asyncio dependency).

Shared test database

For manual checks against real MySQL + Redis, .env.test points everything at a separate schema and namespace (PI_DATABASE_URL → pi_py_test, PI_REDIS_NS=test, PI_WORKSPACE_ROOT → ~/.pi-py/workspaces-test, PI_AUDIT_PATH → ~/.pi-py/audit-test.jsonl). pi/__init__.py only auto-loads .env, so test overrides need an explicit source — and the order matters, because existing environment variables always win:

set -a; . ./.env; . ./.env.test; set +a
pi-py migrate                    # creates/upgrades pi_py_test only
python tools/seed_testdb.py      # idempotent; --reset wipes first, --users N sets the pool
pi-py serve --port 8398          # leave 8300 for production

Seeded fixtures (password from --password, default pi-test-123 — test database only, production has no default password): admin (promoted via UserRepo.set_admin, the only path to admin), alice/bob/carol normal users with one populated and one empty session plus a usage record, overquota (quota_tokens=1000 against 1540 tokens used → 402), and disabled (is_active=0 → 401). The script refuses to run unless PI_DATABASE_URL names a *_test schema, so forgetting the source cannot damage real data. Note that usage is a monthly window and the seeded records are stamped today.

Security note

With PI_SANDBOX unset, tools execute with the server process's own privileges and inherit its environment — PI_JWT_SECRET and PI_DATABASE_URL included. The policy engine is a safety net, not a sandbox: it enforces path confinement and command blocklists, but it cannot stop a command from reading the environment (env, printenv, /proc/self/environ, any interpreter one-liner). Approval gates address mistakes; only a system boundary addresses malice.

The shipped .env therefore sets PI_SANDBOX=docker (network disabled, per-container memory / pids / cpu ceilings, running as the app's own uid). The one known gap that remains: on the cold CLI path (PI_SANDBOX_POOL=0) a timeout kills only the local docker run client — a SIGKILL cannot be forwarded to the container and --rm fires on container exit, so the container keeps running until its own command finishes. (Established by reading the code, not reproduced live; the default warm pool calls docker rm -f and the Engine API path POSTs /kill, so both do tear it down.) For hostile multi-tenant workloads, run tool execution inside microVMs - the cloud compose now defaults to PI_SANDBOX=cubesandbox (half-configured = fail-closed, never a silent fallback; see deploy/cloud-deploy.md §4). Never commit .env (git-ignored); rotate PI_JWT_SECRET in production.

License

MIT.

pi (15838328874/pi)

Searching Database…

200 ·GitHub
What is this?pi-py is a self-hostable, extensible AI agent platform: a Python reimplementation of the 'pi' coding agent harness, shipped as a multi-user HTTP service with sandboxed tool execution, multi-tenant governance, an eval harness, and an RL data flywheel.
Is this a CLI tool I run locally, or a server I deploy?It is a multi-user HTTP server (FastAPI + SSE), not a local CLI/TUI — the old single-user CLI/TUI mode and local SQLite store have been dropped entirely in favor of running it purely as a service.
How do I get it running?Set up a .env with a required database URL and model provider, then start the server with the bundled CLI (pi.cli serve, with pi.cli migrate for schema setup); a Docker Compose stack for MySQL/Redis/Milvus plus a real model is provided for local testing.
What database and LLM backend do I need?A MySQL or PostgreSQL database is mandatory — the server refuses to start without PIDATABASEURL, there is no SQLite fallback outside tests. Any OpenAI-compatible endpoint works for the model (Aliyun MaaS, Qwen, GLM, DeepSeek, Kimi, Doubao), or you can skip API keys entirely with PIMODEL=fake/demo.
Does it run on Windows?No — it's Linux only; Windows and WSL support, along with the old single-user mode, were removed entirely in this version.
What built-in tools does the coding agent have?Twelve built-ins: bash, read, write, edit, grep, find, ls, remember, recall, spawnsubagents, listfiles and fetchfile, with more pluggable via MCP servers, Agent Skills packages, and a RAG search tool.
How does pi-py relate to the original TypeScript pi project?It mirrors pi's package layering in Python: pi-ai becomes pi.llm, pi-agent-core becomes pi.agent, pi-coding-agent becomes pi.tools/pi.prompt, and the daemon/server layer becomes pi.server (FastAPI+SSE) and pi.cli.
What is the RL data flywheel mentioned in the description?Every run's trajectory can feed a rollout-reward-SFT/RLVR JSONL pipeline, turning real agent sessions into training data for fine-tuning or RL.
How does the sandbox stay cheap for sessions that never run a command?VMs are created lazily only on the first bash call (millisecond cold start), reused across turns in a session-level pool, and reclaimed on idle timeout or memory pressure, so memory scales with concurrent active turns rather than total sessions.
Size
5.8 MB
Stars
200
Updated
CommitOct 14
Age
1mo
Commits · 90 Days