7.7 KiB
REPORT — 2026-08-29 15:00 — Bootstrap: runtime container + tool-widening (Redmine 521 + 523)
- turn: FINAL crush turn on the harness repo (q15), Charles directive "alpha asap, then kill the screen/crush stack; bootstrap the container FIRST"
- commits pushed to
ukrrs/MOPACmain:064aed0(brief 495 landing),e1b519d(file tools),e905515(loop wiring + bound + heartbeat),02013d3(runtime container),19d61a0(docs) - the
mopac-loopcompose service is RUNNING and healthy as of writing; it processed alpha task 523 autonomously end-to-end (proof below)
1. Runtime container (Part A)
Files: deploy/Dockerfile.runtime, deploy/compose.yaml (runbook appended
to deploy/runbook.md, README section added).
- Image: digest-pinned
alpine@sha256:143583...(alpine 3.22) + the STATIC release binary (make release, CGO off, built in the digest-pinned golang builder — never on the host) +bash(the bash tool / dev.sh),git(turn commits),docker-cli(dev.sh builder client). No ENTRYPOINT: SIGINT graceful stop is Go's; zombie reaping is docker's built-in init (init: truein compose — docker's tini, zero extra packages, image stays digest-pure). - Same-path bind trick: the repo is mounted at its HOST path
(
HARNESS_REPO, default/home/reachableceo/projects/meta/MOPAC/harness) so a turn's./dev.sh buildinside the container bind-mounts a path the HOST docker daemon understands — builders remain digest-pinned siblings; nothing toolchain-shaped touches the host.harness.tomloverlays read-only (turns can edit the repo, not their own gate). - Secrets:
~/.coordinate/secrets/mopac-loop/(0700):loop.env(0600, HARNESS_REDMINE_KEY + HARNESS_LITELLM_KEY), container.gitconfig+git-credentials(0600, gitea token via credential store — turn pushes authenticate; commits attributed to the vertical). - Placement knobs:
HARNESS_REPO,HARNESS_SECRETS,HARNESS_UID/GID(1001 on this host — the first boot caught the 1000 default and was fixed by parameterizing).
Healthcheck design (no port)
harness loop --status-file state/loop/status.json maintains a heartbeat
JSON — beats on EVERY scan plus a safety ticker at min(poll/3, 30s), so a
long turn (up to the 1800s bound) never stales it past the threshold. The
compose healthcheck is pure mtime age: unhealthy when older than 360s
(= 3x the default 120s poll; keep in sync if the poll changes). Content:
{vertical, pid, started_at, running, scans, dispatched, reports, last_scan_at, last_scan_error, last_beat} — running:false on clean
stop, last_scan_error set while Redmine is down but the daemon lives
(that is deliberately NOT unhealthy: scan errors retry next tick).
Container instructions
cd ~/projects/meta/MOPAC/harness
make release # static binary
docker compose -f deploy/compose.yaml up -d --build # up
docker compose -f deploy/compose.yaml logs -f mopac-loop
docker inspect mopac-loop --format '{{.State.Health.Status}}'
docker compose -f deploy/compose.yaml down # KILL SWITCH
Verified live: state=running restart=unless-stopped health=healthy.
2. Tool-widening (Part B, Redmine 521)
- File tools (
internal/tools/files.go):read(numbered lines, offset/limit, binary refusal, byte cap),edit(exact-match; unique unlessreplace_all; never writes on failure),write(NEW files only — no clobbering),ls,glob(**support, mtime-sorted, capped),grep(RE2, mtime-sorted file list, capped). Deny-first like the bash gate: anything outsidework_root— absolute,.., or symlink escapes (canonicalized via deepest existing ancestor) — returnsErrDenied, counted and fed back to the model with the same semantics as bash denials. Writes are temp+rename atomic. Every call leaves one audit line on stdout (tool name + outcome only, never arguments). - Bash gate: allow-list extended MINIMALLY to the build surface —
./dev.sh *,make check,make release,git add *,git commit *,git push *(bothharness.toml.exampleand liveharness.toml). Enforced byTestShippedAllowListCoversDevAndGit, which parsesharness.toml.exampleitself so the preset cannot drift from the acceptance (dev.sh build/vet/test/check + git commit/push pass; sudo/curl/docker run/python3/go run still default-deny). - Turn budget:
[loop] turn_timeout_secs(default 1800, 0 = off). An expired turn keeps partial content, writes a partial REPORT with stop_reasonturn_timeout, and releases the task (the pre-turn dedup marker prevents hot-looping; PMO re-releases by updating the issue).
3. Test results (./dev.sh check — build + vet + test, docker builder)
All green: brief, config, events, intake, llm, loop, models, quota, serve, tools, writeback. New coverage:
- file-tool gate denials (absolute/
../symlink escapes → ErrDenied; missing file = plain error, not denial), edit exact-match semantics (not-found no-write, ambiguity count, replace_all, old==new refusal), write new-files-only, read offset/limit + truncation, ls/glob/grep scoping + include filter + invalid regexp - timeout expiry: partial REPORT written with
turn_timeout, err chain carries ErrTurnTimeout, zero disables the bound - allow-list acceptance vs the shipped preset (see above)
- heartbeat: file appears + fields,
running:falseafter clean stop, mtime keeps refreshing while scans cycle (poll=1s), scan errors recorded without flipping running - END-TO-END fake-Redmine loop turn: reads a file, EDITS it (verified on disk), runs an allow-listed no-op bash command, REPORTs — note lands on the issue, status transitions per the map, tool audit lines present; a denied write outside the root still completes the turn with the note
4. Live autonomous proof (the alpha queue)
Old dev.sh loop container drained its queue (519 writebacks recovered,
523 turn completed 12:19Z), then was stopped BY NAME and replaced:
docker stop mopac-loop → docker compose up -d. The new container's
scan 2 dispatched re-queued task 523 (the exit-gate ticket itself) at
12:22Z and completed it autonomously at 12:31Z on glm-4.7-flash:
- tools used live: bash (2 ok), glob (2 ok), read (2 ok) — the new file palette in production
- gate denials live:
docker ps -aandscreen -lsrefused with model-facing feedback (default deny held) - REPORT
reports/REPORT-demo-523-20260829-123112.md(rounds=8, tool_calls=8, denied=2, stop=round_limit), Redmine note posted, status New → Resolved - heartbeat counted it:
{"scans":2,"dispatched":1,"reports":1}, health=healthy throughout
5. Notes / handoff
- The killed 495 turn's completed-but-uncommitted work (brief surface, discourse client vendored) was landed first as its own commit so 521 started from a green tree; LIVE discourse delivery on 495 remains open (queue task 519's follow-up belongs to the loop now — it has the tools).
- 523's flash turn hit the 8-round bound while exploring (stop=
round_limit, 23.8k tokens). If turns routinely saturate rounds on
study-class tasks, consider
[loop] max_rounds8 → 12 for the alpha queue; not changed unilaterally. harness.toml(live) updated withturn_timeout_secs = 1800,[tools.files] enabled = true, extended allow-list — file is gitignored, mirrored intoharness.toml.example(committed).- Kill switch documented in compose header, README, runbook:
docker compose -f deploy/compose.yaml down. No broad pkill was used at any point (docker stop mopac-loopby name only). - The crush/screen stack can now be retired per the exit gate: the loop is containerized, restart-policied, healthchecked, and its turns can do build work through the same digest-pinned docker builder discipline.