77</div >
88
99The trusted side of an agent platform: API server, controller, auth,
10- billing, secrets. The untrusted side — the agent runtime itself — is
10+ billing, secrets.
11+ The untrusted side — the agent runtime itself — is
1112` python-agent-harness ` , executed as a ** resident subprocess** per
1213sandbox: ` python-agent-harness serve ` , a bidirectional JSON-lines
13- protocol over stdin/stdout. The harness is never imported; the repos
14- stay decoupled (the harness only needs to be on PATH of whatever runs
15- the agent).
14+ protocol over stdin/stdout.
15+ The harness is never imported; the repos stay decoupled (the harness
16+ only needs to be on PATH of whatever runs the agent).
1617
1718```
1819┌──────────────────────────────────────────────────────────────────────┐
@@ -57,12 +58,12 @@ the agent).
5758└──────────────────────────────────────────────────────────────────────┘
5859```
5960
60- Protocol split: ** JSONL** is the backend↔harness link (pipes);
61- ** SSE ** is the browser↔backend link. The controller is a protocol
62- translator — each parsed JSONL line is fanned out to in-memory
63- subscribers and re-emitted as an SSE ` data: ` frame, so the browser
64- sees the same events the harness TUI renders (tool progress, todos,
65- errors, mid-run questions).
61+ Protocol split: ** JSONL** is the backend↔harness link (pipes); ** SSE **
62+ is the browser↔backend link.
63+ The controller is a protocol translator — each parsed JSONL line is
64+ fanned out to in-memory subscribers and re-emitted as an SSE ` data: `
65+ frame, so the browser sees the same events the harness TUI renders (tool
66+ progress, todos, errors, mid-run questions).
6667
6768## Layout
6869
@@ -114,13 +115,14 @@ uvicorn app.main:app --reload # http://127.0.0.1:8000 (UI at /)
114115```
115116
116117` python-agent-harness ` must be importable-on-PATH as a command; point
117- ` PAW_HARNESS__CMD ` at the absolute binary if it is not. Auth: create
118- a user via ` /auth/register ` , then log in; the UI does this for you.
118+ ` PAW_HARNESS__CMD ` at the absolute binary if it is not.
119+ Auth: create a user via ` /auth/register ` , then log in; the UI does this
120+ for you.
119121
120122## The serve protocol (harness side)
121123
122- One sandbox = one long-lived ` serve ` process. The web side writes
123- ops, the harness answers with events:
124+ One sandbox = one long-lived ` serve ` process.
125+ The web side writes ops, the harness answers with events:
124126
125127```
126128host → harness: {"op": "submit", "prompt": ..., "run_id": ...}
@@ -134,18 +136,18 @@ harness → host: {"type": "ready"} then per-run
134136Because the process is resident: conversation history persists across
135137turns (multi-turn memory), no per-turn interpreter spawn, ` answer `
136138delivers the user's reply to a pending mid-run question (the agent's
137- Question tool / PlanExit confirm), and cancel is a protocol message —
138- no signal semantics.
139+ Question tool / PlanExit confirm), and cancel is a protocol message — no
140+ signal semantics.
139141
140142` notify ` lines carry the progress the UI renders, keyed by ` kind ` :
141- ` tool_start ` (the round's tool names), ` tool_calls ` (the same round
142- with each call's arguments, so a row reads ` Bash(command='ls -la') `
143- rather than a bare ` Bash ` ), ` tool_running ` , ` tool ` , ` todos ` , ` compact ` ,
144- ` retry ` , ` error ` , and ` ask ` for a mid-run question. ` tool_calls ` is
145- additive — a harness that does not send it degrades to the names from
146- ` tool_start ` . ` log ` lines are shown too, except session bookkeeping
147- (the generated session title), which says nothing about what the agent
148- is doing.
143+ ` tool_start ` (the round's tool names), ` tool_calls ` (the same round with
144+ each call's arguments, so a row reads ` Bash(command='ls -la') ` rather
145+ than a bare ` Bash ` ), ` tool_running ` , ` tool ` , ` todos ` , ` compact ` ,
146+ ` retry ` , ` error ` , and ` ask ` for a mid-run question.
147+ ` tool_calls ` is additive — a harness that does not send it degrades to
148+ the names from ` tool_start ` .
149+ ` log ` lines are shown too, except session bookkeeping ( the generated
150+ session title), which says nothing about what the agent is doing.
149151
150152## Configuration (env, prefix ` PAW_ ` )
151153
@@ -166,37 +168,41 @@ is doing.
166168
167169- ** Routes are thin, controllers own the rules** : a route resolves
168170 dependencies, calls a controller, and maps the result to a response
169- schema — nothing else. No route touches the DB, the filesystem or
170- crypto. A controller refuses work by raising from
171- ` controllers/errors.py ` (` NotFound ` , ` Conflict ` , ` InvalidRequest ` ,
172- ` PayloadTooLarge ` , ` Unauthorized ` , ` Forbidden ` ); one handler in
173- ` main.py ` renders that as FastAPI's own ` {"detail": ...} ` shape with
174- the status the error type carries. So business logic never imports
175- ` HTTPException ` , and a controller stays callable from a test, a CLI
176- or a worker thread.
177- - ** Stateless mechanism vs stateful policy** is the ` infra ` /
178- ` controllers ` line, not "core vs supporting". ` infra ` holds
179- primitives with no entities and no session — password hashing, JWT,
180- Fernet ` encrypt ` /` decrypt ` — and imports nothing but ` infra ` .
181- Anything that takes a ` Session ` , reads or writes an ORM entity, or
182- can refuse a request lives in ` controllers ` , even for supporting
183- areas like accounts and secrets. Moving those down would make the
184- bottom layer import ` models ` and ` controllers.errors ` , inverting the
185- dependency direction.
171+ schema — nothing else.
172+ No route touches the DB, the filesystem or crypto.
173+ A controller refuses work by raising from ` controllers/errors.py `
174+ (` NotFound ` , ` Conflict ` , ` InvalidRequest ` , ` PayloadTooLarge ` ,
175+ ` Unauthorized ` , ` Forbidden ` ); one handler in ` main.py ` renders that as
176+ FastAPI's own ` {"detail": ...} ` shape with the status the error type
177+ carries.
178+ So business logic never imports ` HTTPException ` , and a controller
179+ stays callable from a test, a CLI or a worker thread.
180+ - ** Stateless mechanism vs stateful policy** is the
181+ ` infra ` /` controllers ` line, not "core vs supporting".
182+ ` infra ` holds primitives with no entities and no session — password
183+ hashing, JWT, Fernet ` encrypt ` /` decrypt ` — and imports nothing but
184+ ` infra ` .
185+ Anything that takes a ` Session ` , reads or writes an ORM entity, or can
186+ refuse a request lives in ` controllers ` , even for supporting areas
187+ like accounts and secrets.
188+ Moving those down would make the bottom layer import ` models ` and
189+ ` controllers.errors ` , inverting the dependency direction.
186190- ** Decoupling** : the harness is a black-box binary driven by its
187191 documented JSONL protocols (the same ` start ` /` delta ` /` notify ` /
188192 ` log ` /` result ` line shapes on the resident ` serve ` pipe and the
189193 one-shot ` headless --json ` pipe; ` seq ` for ordering, ` run_id ` for
190- correlation, ` usage ` for billing). No imports, no shared state; the
191- web side can be versioned and deployed independently.
192- - ** Runs are protocol turns, not process lifecycles** : one
193- conversation turn = one ` op:submit ` = one ` Run ` row; the resident
194- process survives the run and serves the next turn. Events stream to
195- subscribers over SSE exactly as the harness emitted them (plus run
196- lifecycle events), and the ` result ` line lands in the DB.
194+ correlation, ` usage ` for billing).
195+ No imports, no shared state; the web side can be versioned and
196+ deployed independently.
197+ - ** Runs are protocol turns, not process lifecycles** : one conversation
198+ turn = one ` op:submit ` = one ` Run ` row; the resident process survives
199+ the run and serves the next turn.
200+ Events stream to subscribers over SSE exactly as the harness emitted
201+ them (plus run lifecycle events), and the ` result ` line lands in the
202+ DB.
197203- ** Secrets** are Fernet-encrypted at rest and never returned by the
198- API; they are meant to be injected into the sandbox environment by
199- the sandbox manager (not exposed to agents via the API).
200- - ** Billing** is a token ledger: the controller snapshots
201- ` result.usage ` (input/output/rounds) from the harness into
202- ` usage_events ` , attributed to the user and conversation.
204+ API; they are meant to be injected into the sandbox environment by the
205+ sandbox manager (not exposed to agents via the API).
206+ - ** Billing** is a token ledger: the controller snapshots ` result.usage `
207+ (input/output/rounds) from the harness into ` usage_events ` , attributed
208+ to the user and conversation.
0 commit comments