Skip to content

feat: make AForge exec the default harness - #72

Merged
AbirAbbas merged 10 commits into
mainfrom
agent/aforge-exec-default
Aug 18, 2026
Merged

feat: make AForge exec the default harness#72
AbirAbbas merged 10 commits into
mainfrom
agent/aforge-exec-default

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Status update — agentfield v0.1.130 is released; this PR is ready

Everything below the line was written against the 0.1.129 / rc pins and is kept for history. What is true on the current head:

  • Pins bumped: Python agentfield>=0.1.130 (pyproject + Dockerfile); Go sdk/go v0.1.130 (go/go.mod/go.sum, resolved via the module proxy; go version -m on the image binary shows sdk/go v0.1.130).
  • "Known-inert knobs" are no longer inert: 0.1.130 reads AGENTFIELD_AFORGE_COMMAND (exec default, do opt-in), honours AFORGE_BIN, and passes --timeout/--turns to aforge. Manifest wording updated.
  • Extra fix (pre-existing, found by the live run): go/Dockerfile runtime stage had no WORKDIR, so the non-root praf process ran with cwd / and every cwd-less harness call (planning, coverage, compound dedup, worthiness, intake fallback) failed with mkdir ./.agentfield-out-…: permission denied — after the review had spent its LLM budget. Added WORKDIR /workspaces. Not caused by the aforge switch (identical SDK logic on the previous pin), but the maintained Go node could not finish a review without it.
  • Live verification (real OpenRouter key, default provider, nothing overridden): both images built with default args from agentfield.ai + PyPI (aforge v0.1.0, agentfield 0.1.130); both nodes registered on a fresh agentfield/control-plane:latest (= v0.1.130) and completed a review of a small public PR with dry_run: true (0 comments posted): Python 5 findings / 17.6 min, Go 8 findings / 21.1 min; argv captured live in both containers: aforge exec --json -w /workspaces/<repo> --timeout 1795 --turns 50 --context-fill 60 --completion-reserve 65536 --model … --plan-model …. Go/Python config tests confirm aforge is the code default and PR_AF_PROVIDER=claude-code flips it. Go: build/vet/test/gofmt clean; Python: 107 passed, ruff clean.
  • Un-draft preconditions met (site live, 0.1.130 on PyPI, sdk/go tag present).

Summary

Make AForge exec the default harness in both the Python and the maintained Go
PR-AF nodes, ship the released AForge CLI in both images, and keep OpenCode
installed as a configuration-only rollback.

This revision replaces the two blockers that made the branch unbuildable:

  • The images no longer pull a private GHCR package. Both Dockerfiles used
    FROM ghcr.io/agent-field/aforge-v2:chat-v2-exec AS aforge +
    COPY --from=aforge /aforge. That package is private, so docker build
    returned a 403 for anyone without registry access — including the
    docker-build CI job. Both now use a fetch stage that downloads the
    published, gzipped release asset over HTTPS and verifies it against the
    release checksums.txt before installing it.
  • The Python node is pinned to a PyPI release, not a git commit. The branch
    pinned agentfield to a commit on the adapter branch (and turned on
    hatchling's allow-direct-references to make that legal). The AForge provider
    has shipped on PyPI since 0.1.127, so agentfield>=0.1.129 is enough.

What changed

Docker (Dockerfile, go/Dockerfile)

FROM debian:bookworm-slim AS aforge
ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge
ARG AFORGE_VERSION=v0.1.0
ARG TARGETARCH
# curl the .gz, gunzip to /out/aforge, sha256-verify the DECOMPRESSED binary
# against checksums.txt, chmod +x
...
COPY --from=aforge /out/aforge /usr/local/bin/aforge

Both ARGs are overridable, so CI or a local mirror can serve the assets from
somewhere else without editing the Dockerfile. checksums.txt is copied verbatim
from the aforge-v2 release and hashes the uncompressed binaries, so the
verification runs after gunzip. Both runtime stages already install
ca-certificates, which AForge needs to reach the model provider over HTTPS.

Python SDK pinpyproject.toml and the builder stage of Dockerfile now
use agentfield>=0.1.129; the [tool.hatch.metadata] allow-direct-references
escape hatch (added only for the git dependency) is gone.

Go SDK pingo/go.mod keeps
github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd,
which resolves through proxy.golang.org and is the only pin that carries the Go
AForge adapter today. go mod tidy removed the superseded
v0.1.127-rc.5.0.20260810165835-72f3d00baf58 hashes the branch had left in
go/go.sum.

Docs — the branch dropped a prose paragraph into the middle of the env-var
table in README.md and go/README.md, which truncates the rendered table and
leaves the remaining rows as literal pipe text. Both tables are repaired (prose
moved below), and the duplicate PR_AF_HARNESS_BIN row the branch added to
go/README.md is dropped.

Known-inert knobs until agentfield#905 releases

The Python SDK on PyPI (0.1.129) and the pinned Go SDK do not behave
identically yet:

Knob Go node (pinned pseudo-version) Python node (agentfield 0.1.129)
AGENTFIELD_AFORGE_COMMAND honored (exec | do) inert — the adapter always runs aforge exec
AFORGE_BIN (SDK-level) honored inert at SDK level; PR-AF's own PR_AF_AFORGE_BIN / AFORGE_BIN still work, because src/pr_af/config.py reads them and passes HarnessConfig.aforge_bin
--timeout passed to aforge exec passed not passed — AForge's own 15-minute wall applies

The docs in this PR state that scope rather than implying the Python node reads
those vars. When agentfield#905 ships as a tagged release, bump these exact
lines:

  • pyproject.toml:14"agentfield>=0.1.129", → the #905 release floor
  • Dockerfile:50"agentfield>=0.1.129" \ → the same floor (the constraint
    string itself must change, or Docker's layer cache keeps reinstalling 0.1.129)
  • go/go.mod:8 — the sdk/go pseudo-version → the tagged release, then
    go mod tidy

Validation contract

Observable behaviours this change must exhibit:

  1. docker build . succeeds without any GHCR credentials.
  2. The aforge binary inside both images is byte-identical to the published
    release asset for the target architecture.
  3. A corrupted or mismatched checksums.txt fails the build rather than
    shipping an unverified binary.
  4. aforge --help runs inside both images.
  5. aforge exec --json -w <dir> inside the image returns exit 0 and a JSON
    envelope for a real prompt, given OPENROUTER_API_KEY.
  6. With no PR_AF_PROVIDER set, both nodes resolve the harness provider to
    aforge and point it at the bundled binary.
  7. PR_AF_PROVIDER=opencode still resolves to the OpenCode provider, with the
    OpenCode CLI still present in both images (rollback without a rebuild).
  8. Repo gates stay green: ruff, pytest, go build / vet / test / gofmt.

How it was verified

Locally, against a mirror of the published layout (the assets are not on
agentfield.ai yet — see below), overriding only AFORGE_BASE_URL:

  • (1) docker build -t pr-af-aforge-test . and
    docker build -f go/Dockerfile -t pr-af-go-aforge-test . — both exit 0.
  • (2) sha256sum /usr/local/bin/aforge in both images →
    61217a18135f392d50c337d971fa5b228f436f8df144800b37fbb340020c79f5, matching
    the aforge-linux-amd64 line in the release checksums.txt.
  • (3) Negative test: rebuilt --no-cache --target aforge against a mirror
    whose checksums.txt had the amd64 hash zeroed →
    sha256sum: WARNING: 1 computed checksum did NOT match / aforge: FAILED,
    build exit 1. The verification is not vacuous.
  • (4) docker run --rm --entrypoint aforge <img> --help → exit 0 in both
    images.
  • (5) echo "Reply with exactly OK" | aforge exec --json -w /tmp/w --timeout 120 --budget 20000 --turns 3
    inside both images →
    {"text":"OK","stop":"done","usage":{...},"artifacts":[],"turns":1,...},
    exit 0.
  • (6)/(7) Resolution probes run inside the containers:
    • Python image: pr_af.config.AIIntegrationConfig.from_env()
      provider = aforge; agentfield.harness.providers._factory.build_provider
      AforgeProvider, bin aforge -> /usr/local/bin/aforge;
      opencode -> /home/praf/.opencode/bin/opencode still present. With
      PR_AF_PROVIDER=opencodeOpenCodeProvider.
    • Go image: config.AIConfigFromEnv() + node.BuildAgent(...) +
      harness.BuildProviderresolved provider = aforge,
      *harness.AforgeProvider, bin aforge -> /usr/local/bin/aforge;
      with PR_AF_PROVIDER=opencode*harness.OpenCodeProvider.
  • (8) ruff check src/ scripts/ → all checks passed. pytest → 107 passed.
    In go/: go build ./..., go vet ./..., go test ./... (all packages ok),
    gofmt -l . empty.

CI expectation — docker-build stays red for now

https://agentfield.ai/downloads/aforge/v0.1.0/ currently returns 404. The
host itself is live and already serves the older build-<sha> coordinate — what
is missing is the v0.1.0 publish. Until it lands, the default AFORGE_BASE_URL
has nothing to fetch and the docker-build job will fail on the aforge stage.
lint and go should be green.

Stays in draft until

  1. https://agentfield.ai/downloads/aforge/<version>/ serves
    aforge-linux-{amd64,arm64}.gz + checksums.txt, and docker-build goes
    green on a re-run with no build args.
  2. agentfield#905 ships a tagged release, and the three pin lines listed above
    are bumped so AGENTFIELD_AFORGE_COMMAND, SDK-level AFORGE_BIN and the
    --timeout pass-through stop being inert on the Python node.

Both are external; nothing else in this PR is waiting on review feedback.

Follow-ups (not in this PR)

  • Consider fetching AForge in the af install (non-Docker) path too — today only
    the images bundle it, so a bare-metal af run needs aforge on PATH.
  • Once #905 lands, add a functional test that asserts the Python node passes
    --timeout derived from PR_AF_MAX_DURATION_SECONDS.

🤖 Generated with Claude Code

AbirAbbas and others added 8 commits August 17, 2026 10:54
Both images pulled the AForge binary out of
ghcr.io/agent-field/aforge-v2:chat-v2-exec. That package is private, so
`docker build` failed with a 403 for anyone without registry access —
including CI.

Replace the image stage in the root (Python) and go/ Dockerfiles with a
fetch stage that downloads the published, gzipped release asset over
HTTPS, verifies the decompressed binary against the release
checksums.txt, and chmod +x's it. AFORGE_BASE_URL and AFORGE_VERSION are
build args, so CI or a local mirror can point the fetch elsewhere without
editing the Dockerfile.

Both runtime stages already install ca-certificates, which AForge needs
to reach the model provider over HTTPS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The branch pinned the SDK to a git commit on the adapter branch, which
also forced hatchling's allow-direct-references escape hatch on. The
AForge provider has shipped on PyPI since 0.1.127, so a normal version
floor is enough: 0.1.129 registers the "aforge" provider and honours
HarnessConfig.aforge_bin, which is all src/pr_af/app.py passes.

Verified against the installed wheel: harness.providers._factory
SUPPORTED_PROVIDERS contains "aforge" and builds AforgeProvider from
config.aforge_bin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The branch bumped the sdk/go require but left the previous
v0.1.127-rc.5.0.20260810165835-72f3d00baf58 hashes behind, so go.sum
carried two versions of a module that only one require references.
`go mod tidy` removes exactly those two lines; go.mod is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The branch dropped a prose paragraph into the middle of the env-var
table in both READMEs, which truncates the rendered table at that point
and leaves the remaining rows as literal pipe text. Move the prose below
the table in each file, and drop the duplicate PR_AF_HARNESS_BIN row the
branch added to go/README.md (the table already documented it further
down).

Also state where the AForge binary comes from now (checksum-verified
release asset, pinned by AFORGE_VERSION), and scope
AGENTFIELD_AFORGE_COMMAND honestly: the Go SDK adapter reads it, the
Python SDK on PyPI always runs `exec`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aforge-v2 cut its first semver release (v0.1.0), so the AFORGE_VERSION
default moves off the build-<sha> coordinate onto the tag. Bumping the
string is what busts the fetch layer's cache, so this is what actually
pulls the released binary instead of restoring the stale one.

Both the Python and the Go image fetch the same coordinate, so both ARG
defaults move together. The AgentField SDK pins are deliberately left
alone — they bump on their own release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0.1.130 is the release that ships the AForge-default harness resolution both
nodes rely on, so both pins move to it:

- Python (pyproject.toml, Dockerfile): agentfield>=0.1.129 -> >=0.1.130.
- Go (go/go.mod, go/go.sum): replace the v0.1.127-rc.5 pseudo-version — whose
  commit is not reachable from any branch — with the released module tag
  sdk/go/v0.1.130.

go build/vet/test and gofmt are clean against the released module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… write

The runtime stage runs as praf but never set a WORKDIR, so the process cwd
was root-owned `/`. The Go SDK creates its schema output dir under the cwd
when a harness call carries no Cwd — planning, coverage, compound dedup,
worthiness and the intake fallback all do — and every such call failed with
`creating temp output dir: mkdir ./.agentfield-out-…: permission denied`,
after the review had already spent its LLM budget. /workspaces is
praf-owned and already PR_AF_WORKDIR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on 0.1.130

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AbirAbbas
AbirAbbas marked this pull request as ready for review August 18, 2026 02:27
@AbirAbbas
AbirAbbas merged commit 0bf9f58 into main Aug 18, 2026
3 checks passed
@AbirAbbas
AbirAbbas deleted the agent/aforge-exec-default branch August 18, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant