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:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user