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).
This commit is contained in:
@@ -61,6 +61,8 @@ without a key, so the live run needs exactly one thing:
|
||||
|
||||
## 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).
|
||||
@@ -73,11 +75,65 @@ without a key, so the live run needs exactly one thing:
|
||||
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. Budget gate via LiteLLM spend APIs; cost line in REPORT.
|
||||
5. Streaming with truncation retry + turn resume.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user