From 704f905a903d65938d3af19b884a8130d823e5cd Mon Sep 17 00:00:00 2001 From: reachableceo Date: Sat, 29 Aug 2026 01:22:12 -0500 Subject: [PATCH] docs: serve front door in README + REPORT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++----- REPORT.md | 58 ++++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e125053..c14ac97 100644 --- a/README.md +++ b/README.md @@ -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= HARNESS_LITELLM_KEY= ./dev.sh serve +``` + +publishes port 8090 on the host LAN (override with `-listen`). OWUI +connection settings: base URL `http://: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-` | +| `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
constant-time vkey compare
401 generic on miss"] + A -->|"POST /v1/chat/completions"| C["CATALOG ROUTING
model mopac- -> tier -> concrete model
"] + C --> T["BOUNDED TURN (stateless)
full client history per call
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-` 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-`), 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 diff --git a/REPORT.md b/REPORT.md index 84ff170..6be58d6 100644 --- a/REPORT.md +++ b/REPORT.md @@ -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-` (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.