Files
MOPAC/REPORT.md
T
mrcharles f89b6945e3 docs: events receiver in README, phase 2b status in REPORT
README: dev.sh quickstart (digest-pinned builder), webhook receiver
section (routes, verification, config, smoke), event-path mermaid, CLI
flag table, [events] config rows, status rows. REPORT: phase 2b works/
stubbed list and phase 3 next chunk (event-to-turn dispatch wiring).
2026-08-28 21:38:34 -05:00

140 lines
8.0 KiB
Markdown

# 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).
- Redmine issue-note writeback (SoR note after the REPORT) — file only.
- 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).
- Task selection is first-in-scope; no P1-first ordering or status
transitions, so chained `once` re-picks the same issue (use --task-id).
- 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).
## 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. Redmine note writeback + status transition so chaining advances scope.
4. Event → turn dispatch wiring: stored actionable events actually chain
conductor iterations for the affected stack.
5. Budget gate via LiteLLM spend APIs; cost line in REPORT.
6. Streaming with truncation retry + turn resume.