docs: serve front door in README + REPORT

README: status line, OpenAI front door section (routes table, stateless
semantics, OWUI connection recipe), mermaid flow for the OWUI door, CLI
subcommand + flags rows, [serve] config rows, status table rows (serve
works; streaming + serve tool use stubbed as Next). REPORT.md: phase 3b
entry with the live LAN proof and the Next list.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-08-29 01:22:12 -05:00
parent c9e86eefa8
commit 704f905a90
2 changed files with 140 additions and 9 deletions
+83 -8
View File
@@ -8,12 +8,13 @@ the loop is the worker, no bash middle layer. There is no TUI — humans and
other agents interact with a stack only through Redmine (SoR), Discourse
(docs) and Gitea (code), never by attaching to the loop.
Status: 2026-08-28 — skeleton + event receiver + **self-host loop live**:
`harness loop` polls the intake, runs one bounded turn per new/updated issue,
notes the REPORT back on the issue and transitions status (fake-Redmine e2e
test-asserted); MVP demo path live since earlier the same day (single GLM turn
through LiteLLM to REPORT); `harness events` webhook receiver smoke-proven on
LAN port 4100.
Status: 2026-08-29 — skeleton + event receiver + self-host loop + **OWUI
front door live**: `harness serve` answers OpenAI-compatible
`/v1/models` + `/v1/chat/completions` on LAN port 8090 (bearer vkey,
stateless bounded turns, live-proven through LiteLLM to glm-5.3 and
glm-4.7-flash). Also live: `harness loop` (Redmine SoR self-hosting, fake-
Redmine e2e test-asserted), the MVP demo path, and `harness events` on port
4100.
## Quickstart
@@ -46,10 +47,12 @@ Expected output (tail):
```text
ok ukrrs.com/mopac/harness/internal/config
ok ukrrs.com/mopac/harness/internal/events
ok ukrrs.com/mopac/harness/internal/intake
ok ukrrs.com/mopac/harness/internal/llm
ok ukrrs.com/mopac/harness/internal/loop
ok ukrrs.com/mopac/harness/internal/models
ok ukrrs.com/mopac/harness/internal/serve
ok ukrrs.com/mopac/harness/internal/tools
ok ukrrs.com/mopac/harness/internal/writeback
```
@@ -176,6 +179,53 @@ and the JSONL record, driven with python urllib (curl is banned on host):
./dev.sh smoke
```
### OpenAI-compatible front door (`harness serve`)
OpenWebUI (Cloudron + SSO) is the interactive surface; MOPAC is just another
OpenAI connection to it. Configure `[serve]` in `harness.toml`, then:
```sh
HARNESS_SERVE_VKEY=<vkey> HARNESS_LITELLM_KEY=<vertical key> ./dev.sh serve
```
publishes port 8090 on the host LAN (override with `-listen`). OWUI
connection settings: base URL `http://<host>:8090/v1`, API key = the vkey.
Routes:
| Route | Auth | Behavior |
|---|---|---|
| `POST /v1/chat/completions` | Bearer vkey | one bounded stateless conductor turn over the sent history; reply = single assistant message + usage |
| `GET /v1/models` | Bearer vkey | the servable catalog: one model per `[models.classes]` class, named `mopac-<class>` |
| `GET /healthz` | none | liveness |
Semantics (v0):
- **Stateless**: OWUI sends the full conversation history each call; the
harness runs ONE bounded turn over it (same `runTurn` machinery `once`
uses) and returns the final text. No session storage.
- **Model routing**: `request.model` (e.g. `mopac-primary`) maps through the
existing class -> tier -> concrete-model tables; unknown model = 400
naming the valid ones. `[serve] enabled_models` optionally narrows the
catalog.
- **History assembly**: the client's leading system message is preserved
verbatim (OWUI personas win); only when none is sent does the harness
prepend a minimal identity prompt for the vertical.
- **Auth**: the Bearer vkey is compared as SHA-256 digests in constant time;
missing/wrong keys get one byte-identical generic 401. The vkey is a ref
(`vkey_ref`), resolved at startup, never logged.
- **Non-streaming**: `stream:true` gets an explicit 400 (OWUI tolerates
non-streaming providers); streaming is Next.
- **Tools OFF**: serve turns are pure chat — any tool call a model produces
anyway is refused as a tool result, never executed. Tool use inside serve
turns is Next.
- `temperature` / `max_tokens` are forwarded upstream; usage is summed over
rounds and returned with the reply; upstream failures surface as a terse
502 (upstream bodies never reach the client).
Live on the LAN 2026-08-29: catalog of 9 models, 401/400/200 paths, and real
turns through LiteLLM (`mopac-primary` -> glm-5.3, `mopac-study` ->
glm-4.7-flash) with usage accounting, driven by python urllib.
### Help
```sh
@@ -247,6 +297,18 @@ Verification rejects unsigned and unverified deliveries with 401 (no detail
beyond "unverified"); secret material and header values never reach logs or
the event log — only normalized fields and a payload digest do.
OWUI chat is a second door into the same loop (the bounded-turn core is
shared, tools off):
```mermaid
flowchart LR
O(["OpenWebUI (Cloudron + SSO)"]) -->|"Bearer vkey"| A["AUTH<br/>constant-time vkey compare<br/>401 generic on miss"]
A -->|"POST /v1/chat/completions"| C["CATALOG ROUTING<br/>model mopac-<class> -> tier -> concrete model<br/>"]
C --> T["BOUNDED TURN (stateless)<br/>full client history per call<br/>tools OFF v0"]
T -->|"assistant message + usage"| O
A -->|"GET /v1/models"| M(["catalog: one model per class"])
```
## CLI reference
Subcommands (from `harness help`):
@@ -257,6 +319,7 @@ Subcommands (from `harness help`):
| `harness once` | run ONE conductor iteration, then exit |
| `harness loop` | run the self-host daemon until SIGINT (poll -> turn -> note/status writeback) |
| `harness events` | run the webhook receiver until SIGINT/SIGTERM |
| `harness serve` | run the OpenAI-compatible front door until SIGINT/SIGTERM (the OWUI connection) |
Flags for `once`:
@@ -283,6 +346,13 @@ Flags for `events`:
| `-config PATH` | config file (default `$HARNESS_CONFIG`, then `./harness.toml`) |
| `-listen ADDR` | bind address (overrides `[events]` listen) |
Flags for `serve`:
| Flag | Meaning |
|---|---|
| `-config PATH` | config file (default `$HARNESS_CONFIG`, then `./harness.toml`) |
| `-listen ADDR` | bind address (overrides `[serve]` listen) |
Exit codes:
| Code | Meaning |
@@ -327,6 +397,9 @@ cannot rot):
| `[events.redmine]` | `secret_ref`, `secret_header` | shared-secret ref + header name (default `X-Redmine-Webhook-Secret`) |
| `[events.discourse]` | `secret_ref`, `secret_header` | shared-secret ref + header name (default `X-Discourse-Webhook-Secret`) |
| `[events.gitea]` | `secret_ref` | HMAC secret ref; verification is always `X-Gitea-Signature` (hex HMAC-SHA256 of the raw body) |
| `[serve]` | `listen` | `harness serve` bind address (default `:8090`; own port, coexists with `[events]`) |
| | `vkey_ref` | bearer vkey ref for the OpenAI-compatible front door (the key the OWUI connection presents) |
| | `enabled_models` | optional subset of the catalog (`mopac-<class>` names); empty = every `[models.classes]` class
Key refs accepted anywhere a `*_ref` appears: `env:NAME`, `file:PATH`,
`literal:VALUE` (last resort), `mpk:PLACEHOLDER` (resolved through the
@@ -379,6 +452,7 @@ Sourced from [REPORT.md](REPORT.md) — keep both in sync.
| Exec tool | Works | allow-listed bash, segment-wise compound checks, timeout + truncation |
| REPORT writeback | Works | timestamped file + `REPORT-latest.md`, atomic, full telemetry |
| Event receiver | Works | `harness events`: verify (HMAC/shared-secret) → normalize → append-only JSONL with provider-id dedup → action mapping; smoke-proven on LAN port 4100 |
| OpenAI front door | Works | `harness serve`: `/v1/models` catalog (one model per class, `mopac-<class>`), stateless bounded `/v1/chat/completions` turns, constant-time vkey auth, tier-map routing, usage accounting; live-proven on LAN port 8090 via LiteLLM (glm-5.3 + glm-4.7-flash) |
| Self-host loop | Works | `harness loop`: poll intake → sequential bounded turns → REPORT note writeback → status map → dedup by id + updated_on (append-only loop.jsonl); `--once`, `--dry-run`, `-interval`; fake-Redmine e2e test-asserted |
| Redmine note writeback | Works | loop path POSTs the REPORT body as a journal note (PUT `/issues/{id}.json`); `once` stays file-only |
| Status transitions | Works | `[redmine.status_map]` names → ids via `/issue_statuses.json`; refresh advances the dedup marker past its own writes |
@@ -389,11 +463,12 @@ Sourced from [REPORT.md](REPORT.md) — keep both in sync.
| Budget/semaphore gate | Stubbed | tokens in REPORT, no cost/spend enforcement yet |
| `bw:` key refs | Stubbed | error until the bitwarden wrapper (phase 3) |
| Loop concurrency | Stubbed | v0 = one turn at a time; in-process concurrency knob later |
| Streaming + turn resume | Stubbed | retry is request-level today |
| Streaming + turn resume | Stubbed | retry is request-level today; serve door is non-streaming by design v0 |
| Serve tool use | Stubbed | serve turns are pure chat (tools off); wiring the gated tool palette into serve turns is Next |
| Session persistence/repair | Stubbed | not started |
| Write confinement | Stubbed | declared roots + symlink checks land with the permission layer |
| Local inbox intake | Stubbed | not started |
| Next (phase 3) | Next | bitwarden wrapper, full permission layer, event → turn wiring, budget gate via LiteLLM spend APIs, streaming with resume, loop concurrency knob |
| Next (phase 3) | Next | bitwarden wrapper, full permission layer, event → turn wiring, budget gate via LiteLLM spend APIs, serve streaming + serve tools, loop concurrency knob |
## Docs and links
+57 -1
View File
@@ -191,6 +191,60 @@ slot-file cap; `~/.coordinate/scripts/queue-next.sh`, `queue-after.sh`,
it targets screen `reachableceo-PMO`, the live screen is `RCEO-PMO`, so
the message was silently dropped).
## Build phase 3b — OWUI front door (`harness serve`) — 2026-08-29
Charles' TASK (2026-08-29 00:00): OpenAI-compatible front door, OWUI is the
client; DESIGN "OWUI front door" (hermes killed). OWUI chat and webhooks are
two doors into the same conductor loop.
Works:
- **`harness serve` CLI**: stdlib net/http receiver in the events style,
SIGINT/SIGTERM graceful shutdown, `-config` / `-listen` flags, own port
(`[serve] listen`, default `:8090`) so it coexists with `harness events`.
Routes: `POST /v1/chat/completions`, `GET /v1/models`, `GET /healthz`.
- **Catalog = the class -> tier map**: every `[models.classes]` class is
served as model `mopac-<class>` (mopac-study, mopac-code, mopac-primary,
...); the request's model routes through the SAME tier table `once` uses
to a concrete proxy model. Unknown model = 400 naming the valid ones.
`[serve] enabled_models` optionally narrows the catalog (validated at
load).
- **Stateless bounded turns**: OWUI sends the full history each call; the
server runs ONE conductor turn over it (shared `runTurn` after a clean
refactor — `turn()` and `ServeTurn` both delegate; no copy-paste) and
returns the final text as a single assistant message + usage (summed over
rounds). No session storage. Client system message preserved verbatim; a
minimal vertical-identity system prompt is prepended only when the
history has none.
- **Auth**: Bearer vkey (`[serve] vkey_ref`, refs only, resolved at
startup), SHA-256 digest compare in constant time; missing/malformed/
wrong keys get one byte-identical generic 401; vkey never logged, never
in any response.
- **v0 knobs**: `temperature` + `max_tokens` forwarded upstream; `stream:
true` = explicit 400 (OWUI tolerates non-streaming providers); tools
OFF — a hallucinated tool call is refused as a tool result and counted,
never executed; upstream failures = terse 502 (bodies never forwarded).
- **dev.sh serve**: digest-pinned builder, host port 8090 published,
`HARNESS_SERVE_VKEY` / `HARNESS_LITELLM_KEY` passed through.
- **Tests**: scripted fake OpenAI upstream against the REAL server
(httptest): auth matrix (401 paths + no-leak), catalog + subset,
end-to-end chat (concrete model out, no tools on the wire, OpenAI
response shape, usage), history assembly both ways, multi-round usage
sums with refused tool-call feedback, knob forwarding, 400/502 table,
fail-fast construction. Loop-package tests cover ServeTurn prepend/
preserve/tools-off/knobs.
- **Live LAN proof (2026-08-29)**: port 8090, python urllib driver —
healthz 200, wrong key 401, catalog 200 (9 models), unknown model 400
naming them, stream 400, real turns through LiteLLM: `mopac-primary` ->
glm-5.3 (255 tokens) and `mopac-study` -> glm-4.7-flash (166 tokens),
audit lines clean.
Stubbed / Next:
- Streaming (SSE) on the serve door; tool use inside serve turns (gated
palette per vertical); per-connection vkeys via LiteLLM/keyproxy minting
(today one vkey per harness instance).
## Next chunk (phase 3)
1. bitwarden-go wrapper + `bw:` refs (unblocks secret posture).
@@ -199,5 +253,7 @@ the message was silently dropped).
3. Event → turn dispatch wiring: stored actionable events actually chain
conductor iterations for the affected stack.
4. Budget gate via LiteLLM spend APIs; cost line in REPORT.
5. Streaming with truncation retry + turn resume.
5. Streaming with truncation retry + turn resume (serve door SSE included).
6. Loop concurrency knob (in-process limit) once load justifies it.
7. Tool use inside serve turns (per-vertical gated palette) + per-connection
vkey minting.