Files
MOPAC/REPORT.md
T
mrcharles 704f905a90 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
2026-08-29 01:22:12 -05:00

15 KiB

REPORT — harness skeleton (build phase 2) — 2026-08-28

Works

  • Config: harness.toml via a stdlib-only TOML-subset parser (tables, bare keys incl. hyphens, strings/ints/bools, single- and multi-line arrays; trailing commas tolerated; anything richer fails loudly with line numbers). Defaults + validation; secrets are refs only (env:NAME / file:PATH / literal:VALUE; bw: reserved) and redacted from every error path. harness.toml.example is tracked and load-tested so it can never rot; real configs are gitignored.
  • Model routing v0: [models] tier map + [models.classes] class map; requests go out with the CONCRETE model resolved from the map (mopac-study→glm-4.7-flash, mopac-code→glm-5.2, mopac-review→glm-5-turbo, mopac-primary→glm-5.3, + mopac-vision→glm-4.6v). No heuristic code; unknown class = hard error naming the config section.
  • Conductor single-shot: harness once = intake → routing → bounded turn → REPORT, then exit 0. Chainable, no daemon. --dry-run = intake + plan (resolved model, tool bounds) with ZERO LLM calls (test-asserted). --task-id filter for chaining. Exit codes: 0 ok/no-tasks, 1 config/usage, 2 intake, 4 llm/turn.
  • Intake: Redmine /issues.json scope query (raw filter params or saved query id), task class from a configurable custom field with default-class fallback; --demo builds the issue from [demo] (no Redmine needed).
  • Bounded turn: OpenAI-compatible chat via LiteLLM (stdlib http; base_url ± /v1 normalized; Bearer auth; retry/backoff on 429/5xx/ transport; usage accounting), tool-calling loop capped at max_rounds; gate denials feed back to the model as tool results and are counted, not fatal. On mid-turn LLM failure after content exists, a partial REPORT is still written with the error as stop reason.
  • Exec tool: allow-listed bash. maki-derived scope semantics without tree-sitter: compound commands split segment-by-segment (&&/||/;/ |, quote-aware), cmd * word-boundary, pfx* raw prefix, pfx/** path prefix, * universal; deny beats allow; $()/backticks/subshells always denied (headless has no prompt channel). Per-command timeout with process-group cleanup on unix; output truncation.
  • Writeback: REPORT-<vertical>-<task>-<ts>.md + REPORT-latest.md (atomic tmp+rename) with model/tier/class/tokens/rounds/denied/stop telemetry per the DESIGN auditability bar.
  • Tests: table-driven, stdlib testing only — TOML subset (valid doc
    • 9 error cases), config defaults/validation/key refs (incl. leak check), routing decisions, scope matcher + gate (14 cases) + exec (timeout, truncation, deny), writeback, Redmine intake (httptest), LLM client (auth/model/retry/4xx/empty-choices), and loop end-to-end against a scripted fake OpenAI server (demo turn, dry-run zero-call, tool round-trip with message-shape assertions, denial counting, round limit, error-class mapping). go build ./..., go vet ./..., go test ./... all clean on go1.26.7; every intermediate commit builds standalone.

MVP demo bar status

harness once --demo is wired end-to-end: prompt "tell me about yourself" → LiteLLM (glm-5.3 via mopac-primary) → GLM self-description → REPORT. Proven against a scripted fake in tests. The live proxy (http://192.168.3.78:4001) is reachable from this host and answers 401 without a key, so the live run needs exactly one thing:

export HARNESS_LITELLM_KEY=<vertical virtual key>
./bin/harness once --demo

Stubbed / known gaps

  • Event → turn dispatch: harness events stores + maps events and hands them to Conductor.DispatchEvent, which is a printing stub (phase 3).
  • Budget/semaphore GATE (LiteLLM spend APIs, class-aware slots) and cost in REPORT (tokens only today).
  • bw: key refs error until the bitwarden wrapper (phase 3).
  • Streaming + turn resume on truncation — retry is request-level today.
  • Session persistence + read-time repair (crush notes §2) — not started.
  • Write confinement to declared roots + symlink-aware canonicalization; redirects currently match by segment text (phase 3 permission layer).
  • once task selection is first-in-scope (--task-id to chain); the loop's dedup + status map supersede this for self-hosted runs.
  • Local inbox intake (DESIGN core-loop step 1, second half) — not started.

Build phase 2b — events receiver (mopac events) — 2026-08-28

Works:

  • harness events CLI: stdlib net/http receiver (no frameworks), runs until SIGINT/SIGTERM, graceful shutdown, -config / -listen flags. Routes POST /hooks/{redmine,discourse,gitea} + GET /healthz; GET on a hook = 405, unknown path = 404, oversized body (> 1 MiB) = 413.
  • Verification, deny-first: gitea = hex HMAC-SHA256 of the raw body in X-Gitea-Signature (constant-time hmac.Equal); redmine/discourse = shared-secret header (constant-time compare; header names configurable, defaults X-Redmine-Webhook-Secret / X-Discourse-Webhook-Secret). Unsigned/unverified = 401 with a single generic error body; rejection logs name the failure class only, never header values or secrets.
  • Normalization: one internal Event (source, kind, actor, canonical subject id redmine:issue:42 / discourse:topic:7 / gitea:pr:ukrrs/MOPAC#5, title, repo, provider event id, payload sha256 digest, received-at) — tolerant extraction across known payload variants (redmine_webhooks + flat shapes, Discourse headers + payload, Gitea action/pull_request/issue shapes incl. closed+merged → pr_merged).
  • Action mapping (DESIGN): redmine issue update/note/journal → dispatch_turn; discourse post reply → respond_turn; gitea PR approved/merged → pipeline_step; everything else stored-but-ignored.
  • Persistence: append-only state/events/events.jsonl (0600, dir 0700), dedup by provider event id (X-Gitea-Delivery, X-Discourse-Event-Id, X-Redmine-Delivery when present; payload digest fallback). Dedup index rebuilt from the file at startup (torn tail line skipped), so replays across restarts still dedup — at-least-once delivery, exactly-once reaction.
  • Config: [events] listen/state_dir + per-source secret refs (env:/file:/literal:, resolved at startup; server refuses to start with zero secrets). Full validation, ref values never echoed.
  • dev.sh: every build/vet/test/run path routes through the digest-pinned builder golang@sha256:e8c859f... (= golang:1.26-bookworm; alpine has no bash for the exec tool's tests).
  • Tests: table-driven — signature verification (7 HMAC + 5 shared secret cases), normalization (13 payload/shape cases + action map), store dedup/restart/torn-tail/permissions, end-to-end httptest (401/400/ 200 paths, replay dedup, ignore-not-dispatched, oversized body, log/JSONL secret-leak assertions). Docker build/vet/test clean.
  • Smoke (live, LAN port 4100): containerized receiver driven by host python3 urllib (curl banned on host): unsigned → 401, bad HMAC → 401, wrong shared secret → 401, valid → 200 stored + JSONL line, replay → 200 duplicate, per-provider actions correct, stub dispatch fired once per stored actionable event. ./dev.sh smoke reproduces it end-to-end.

Stubbed:

  • Event → turn dispatch (Conductor.DispatchEvent prints the would-be action; real wiring lands in phase 3 per plan).
  • Cross-host/event forwarding (T1 replication later; JSONL is local-only).

Build phase 3a — self-host core (harness loop) — 2026-08-28 ~22:00

Charles (21:30): "get to be self hosting asap — smallest core that can bootstrap itself (redmine/gitea interaction fine, discourse later)". The harness now drives ITSELF: Redmine is the SoR, the loop is the worker, no bash middle layer.

Works:

  • harness loop CLI: the self-host daemon. Polls the /issues.json intake every [loop] poll_interval_secs (default 120; -interval override; --once = single scan for cron-style setups; --dry-run = scan + print, zero LLM calls, zero state writes — test-asserted).
  • Dispatch discipline: one bounded turn per issue not yet processed at its current updated_on, strictly sequential (v0 — the concurrency slot-file cap of the old stack is replaced by "one at a time"; a knob can come later). The dedup marker is written BEFORE the turn, so a failed turn is recorded and never retried (update the issue to re-release it) — a down proxy cannot hot-loop the poll.
  • State: append-only state/loop/loop.jsonl (0600, dir 0700), one JSON line per action (dispatch / report / note / status / commit / refresh / error), index rebuilt at startup (torn tail skipped). Restart keeps exactly-once-reaction semantics — test-asserted across a new Conductor instance.
  • Redmine note writeback (was the phase-2 stub): after the turn, the REPORT body is POSTed onto the issue as a journal note (PUT /issues/{id}.json), so the SoR carries the result, not just the filesystem.
  • Status transitions: [redmine.status_map] (quoted TOML keys, e.g. "In Progress" = "Done"); names resolve to ids via /issue_statuses.json (fetched once, cached, case-insensitive); unmapped statuses are left alone.
  • Self-retrigger prevention: the loop's own note/status writes bump updated_on; after writeback the loop re-fetches the issue and advances its dedup marker to the post-writeback updated_on (refresh event). If that refresh call fails, the JSONL records the error and one redundant re-dispatch may follow (visible, bounded).
  • mpk: key refs ([keyproxy]): resolved through the ukrrs/mopac-keyproxy hop — POST /v1/resolve, bearer token from a LOCAL token_ref (no recursion), 60s in-memory cache, transport errors query-string-stripped so a misconfigured hop cannot echo the token. env:/file:/literal: refs untouched: the loop runs with or without keyproxy up. All hosts in harness.toml, none in code.
  • Gitea REPORT commit (optional, off by default): [gitea] commit_reports = true commits each REPORT file right after it lands (contents API: GET for sha → POST create / PUT update, token auth, branch from config).
  • Module rename: git.knownelement.com/reachableceo/MOPAC/harnessukrrs.com/mopac/harness (the doc-rot finding folded in; build + vet
    • test verified in the Docker builder).
  • Tests: loop e2e against a fake Redmine (mutable state: notes bump updated_on like the real SoR) + fake OpenAI server — dispatch/note/ status happy path, dedup on rescan, re-dispatch on PMO issue update, failed turn recorded without hot-loop, dry-run zero-trace, restart dedup survival, status-map miss, gitea create/update/auth shapes, keyproxy resolver (cache hit, auto-prefix, 401/404, local refs untouched), writeback unit tests for PUT body shapes.

Replaced by this turn (die list for the PMO report; deletion pending Charles' sign-off): /home/_crossfeed/tooling/agent-stack/semaphore.sh + slot-file cap; ~/.coordinate/scripts/queue-next.sh, queue-after.sh, pmo-heartbeat.sh; the dispatch-turn.sh screen doorbell (already dead — 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).
  2. Full permission layer: tree-sitter bash scopes, write roots, symlink checks, per-vertical allow/deny presets.
  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 (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.