Skip to content

Commit 80f8013

Browse files
committed
Add trusted-side backend: controller, auth, billing, secrets
Web/API platform driving python-agent-harness as an untrusted subprocess (headless --json JSON-lines protocol; the harness is never imported). - app/controller: run lifecycle, SSE fan-out with replay, protocol parser (sole harness coupling), LocalRunner (subprocess exec, process-group SIGINT/CTRL_BREAK cancel, idle reaper) - app/routes: JWT auth (register/login/refresh/me/admin), conversation CRUD + run start/cancel/stream, usage-ledger billing, write-only Fernet secrets - app/core: settings, security primitives, secrets store - models: users, conversations, runs (delete cascade), usage events, secrets, sandboxes; SQLite WAL dev default - static/minimal chat UI; 96 tests, coverage 96%, ruff + pyright clean Deferred: Docker sandbox runner.
1 parent bf5216c commit 80f8013

30 files changed

Lines changed: 3648 additions & 3 deletions

‎.gitignore‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
__pycache__/
2+
*.pyc
3+
.coverage
4+
htmlcov/
5+
.pytest_cache/
6+
.ruff_cache/
7+
*.db
8+
*.db-shm
9+
*.db-wal
10+
.env
11+
.venv/

‎README.md‎

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,104 @@
22

33
# python-agent-web
44

5-
**web-based ai-agent platform.**
5+
**Web/API platform for python-agent-harness.**
66

77
</div>
88

9-
A web-based ai-agent platform.
9+
The trusted side of an agent platform: API server, controller, auth,
10+
billing, secrets. The untrusted side — the agent runtime itself — is
11+
`python-agent-harness`, executed as a **subprocess** with
12+
`python-agent-harness headless --json` and consumed as a JSON-lines
13+
event stream. The harness is never imported; the repos stay decoupled
14+
(the harness only needs to be on PATH of whatever runs the agent).
1015

11-
.
16+
```
17+
Coding Space Server
18+
│
19+
┌──────▼─────────┐
20+
│ Agent Controller│ app.controller
21+
└──────┬─────────┘
22+
│ subprocess: python-agent-harness headless --json
23+
┌──────▼─────────┐
24+
│ Sandbox │ app.controller.runner (LocalRunner now, Docker later)
25+
│ agent process │
26+
│ tools, bash, … │
27+
└─────────────────┘
28+
29+
TRUSTED UNTRUSTED / ISOLATED
30+
───────────── ─────────────────────
31+
FastAPI app harness agent process
32+
Controller agent-generated commands
33+
Auth (JWT) repository, builds
34+
Billing (usage ledger)
35+
Secrets (Fernet)
36+
Runner (sandbox manager contract; LocalRunner today, Docker later)
37+
```
38+
39+
## Layout
40+
41+
```
42+
app/
43+
main.py FastAPI app factory + routers + static UI
44+
core/
45+
config.py Settings (pydantic-settings, env prefix PAW_)
46+
security.py JWT auth (access/refresh) + password hashing
47+
db.py SQLite engine/session (SQLAlchemy ORM)
48+
models.py User, Conversation, Run, UsageEvent, Secret
49+
schemas.py Pydantic request/response models
50+
routes/
51+
auth.py POST /auth/register /auth/login /auth/refresh /auth/me
52+
conversations.py CRUD + POST /{id}/runs (start) + GET /{id}/stream (SSE)
53+
billing.py usage summary (token ledger)
54+
secrets.py CRUD (write-only read: value never returned)
55+
controller/
56+
protocol.py Parse harness --json lines (seq/run_id/result/usage)
57+
manager.py Controller: start_run, event pump, subscribe, cancel
58+
runner.py Runner protocol + LocalRunner (subprocess exec, cancel)
59+
static/index.html Minimal chat UI (EventSource -> runs, fetch -> API)
60+
```
61+
62+
## Quick start
63+
64+
```bash
65+
python -m venv .venv && . .venv/bin/activate
66+
pip install -e ".[dev]"
67+
uvicorn app.main:app --reload # http://127.0.0.1:8000 (UI at /)
68+
```
69+
70+
`python-agent-harness` must be importable-on-PATH as a command; point
71+
`PAW_HARNESS__CMD` at the absolute binary if it is not. Auth: create
72+
a user via `/auth/register`, then log in; the UI does this for you.
73+
74+
## Configuration (env, prefix `PAW_`)
75+
76+
| var | default | note |
77+
|---|---|---|
78+
| `PAW_DB_URL` | `sqlite:///./paw.db` | any SQLAlchemy URL |
79+
| `PAW_SECRET_KEY` | dev default | **set in production** |
80+
| `PAW_ACCESS_TOKEN_MINUTES` | `30` | JWT access TTL |
81+
| `PAW_REFRESH_TOKEN_DAYS` | `14` | JWT refresh TTL |
82+
| `PAW_HARNESS__CMD` | `python-agent-harness` | harness binary |
83+
| `PAW_HARNESS__CWD` | `""` | agent workspace dir per run |
84+
| `PAW_HARNESS__MAX_ROUNDS` | unset | round budget forwarded to `--max-rounds` |
85+
| `PAW_HARNESS__TIMEOUT` | unset | wall-clock budget forwarded to `--timeout` |
86+
| `PAW_RUNNER` | `local` | `local` only; docker later |
87+
| `PAW_SANDBOX__TTL_SECONDS` | `300` | idle reaper TTL |
88+
89+
## Design notes
90+
91+
- **Decoupling**: the harness is a black-box binary driven by its
92+
documented `headless --json` protocol (`start`/`delta`/`notify`/
93+
`log`/`result` lines, `seq` for ordering, `run_id` for correlation,
94+
`usage` for billing). No imports, no shared state; the web side
95+
can be versioned and deployed independently.
96+
- **Runs are processes**: one conversation turn = one harness exec =
97+
one `Run` row; events stream to subscribers over SSE exactly as the
98+
harness emitted them (plus run lifecycle events), and the `result`
99+
line lands in the DB.
100+
- **Secrets** are Fernet-encrypted at rest and never returned by the
101+
API; they are meant to be injected into the sandbox environment by
102+
the sandbox manager (not exposed to agents via the API).
103+
- **Billing** is a token ledger: the controller snapshots
104+
`result.usage` (input/output/rounds) from the harness into
105+
`usage_events`, attributed to the user and conversation.

‎app/__init__.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""python-agent-web: trusted-side web/API platform for python-agent-harness."""
2+
3+
__version__ = "0.1.0"

‎app/controller/__init__.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Controller package: protocol, runner, manager."""
2+
3+
from .manager import Controller
4+
from .protocol import ProtocolEvent, RunOutcome, apply_event, parse_line, parse_stream
5+
from .runner import ExecResult, LocalRunner, Runner, get_runner
6+
7+
__all__ = [
8+
"Controller",
9+
"ExecResult",
10+
"LocalRunner",
11+
"ProtocolEvent",
12+
"RunOutcome",
13+
"Runner",
14+
"apply_event",
15+
"get_runner",
16+
"parse_line",
17+
"parse_stream",
18+
]

0 commit comments

Comments
 (0)