feat: make AForge exec the default runtime - #137
Open
AbirAbbas wants to merge 10 commits into
Open
Conversation
Adds `aforge` to RUNTIME_VALUES and to every place a runtime is accepted or
mapped: the alias normaliser (`aforge` / `aforge_v2` / `aforge-v2`), both
harness mappings (provider and adapter both resolve to `aforge`, which is what
the AgentField SDK's provider factory registers), the Build/Execution/Issue/Fast
config `runtime` literals, and the per-runtime model table — where aforge shares
open_code's OpenRouter default (openrouter/deepseek/deepseek-v4-flash-0731).
Flips the OpenRouter auto-selection target from `open_code` to `aforge`, on both
the main and the fast path. The precondition is unchanged in this commit: an
OpenRouter key, no Anthropic key, no explicit SWE_DEFAULT_RUNTIME.
`SWE_DEFAULT_RUNTIME=open_code` remains the configuration-only rollback and
OpenCode stays installed in the image.
Also fixes an inverted comment in the fast-path test ("an OpenRouter key with no
OpenRouter key" -> "no Anthropic key").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #137 changed only the Python node, so the two implementations of `swe-planner` disagreed about what an OpenRouter-only install resolves to — Python said `aforge`, Go still said `open_code` — while the shared README and manifests describe one product. This ports the same change into go/: - runtimex: `aforge` joins RuntimeValues (first, matching Python's tuple order, which is joined verbatim into the "Valid runtimes: ..." error), the alias set (`aforge` / `aforge_v2` / `aforge-v2`), and both harness mappings. - config: an `aforge` base-model row sharing open_code's OpenRouter default, and DefaultRuntime / DefaultFastRuntime auto-select `aforge`. - fast: the fast-path runtime→provider map gains the `aforge` case, matching fast/app.py::_runtime_to_provider. Known gap, documented at DefaultRuntime and in go/README.md: the Go SDK pinned by AGENTFIELD_SDK_REF has no aforge harness provider — `harness.BuildProvider("aforge")` returns `unknown harness provider: "aforge" (supported: claude-code, codex, gemini, opencode)`. The Go node therefore needs AGENTFIELD_SDK_REF bumped to a release carrying agentfield#905 before the new default is usable; until then SWE_DEFAULT_RUNTIME must point it at another runtime. This is one of the gates keeping the PR in draft. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces `FROM ghcr.io/agent-field/aforge-v2:chat-v2-exec AS aforge` +
`COPY --from=aforge /aforge` with a fetch stage. The aforge-v2 source repo is
private and no such image is published, so the image contract could never be
satisfied; the release is distributed as gzipped binaries on the public
download host instead.
The new `aforge` stage (debian:bookworm-slim + curl/ca-certificates):
1. downloads ${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${TARGETARCH}.gz
2. gunzips it to /out/aforge
3. downloads checksums.txt — whose hashes are of the DECOMPRESSED binaries —
rewrites the matching "<sha256> aforge-linux-<arch>" line to name the
local file, and runs `sha256sum -c`
4. chmod +x and smoke-runs `aforge --help` so a corrupt or wrong-arch
download fails the build rather than the deploy
`COPY --from=aforge /out/aforge /usr/local/bin/aforge` is unchanged in spirit.
Both ARGs stay overridable (`--build-arg AFORGE_BASE_URL=... AFORGE_VERSION=...`)
so CI, a mirror, or an air-gapped registry can point elsewhere; AFORGE_VERSION
is part of the layer's cache key, so bumping it is what pulls a newer AForge.
Applied to go/Dockerfile too — the Go node ships the same CLI surface — and
ca-certificates is now explicit in the Python runtime stage, since aforge
speaks HTTPS to openrouter.ai.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #137 pinned all three dependency files to a git SHA on the agentfield#905 branch (`agentfield @ git+...@bfd34426#subdirectory=sdk/python`). That commit is unreleased and lives only on a draft PR branch, so the pin is not installable from any published index and would break `pip install swe-af`. The aforge harness provider itself has shipped since agentfield 0.1.127, so the floor moves to the published `agentfield>=0.1.129` instead. Verified: a clean `pip install -e ".[dev]"` resolves agentfield 0.1.129, whose harness.providers._factory registers "aforge" in SUPPORTED_PROVIDERS and builds an AforgeProvider. What is NOT yet in a release, and what to bump when it is (all three lines): pyproject.toml:13 "agentfield>=0.1.129", requirements.txt:5 agentfield>=0.1.129 requirements-docker.txt:5 agentfield>=0.1.129 0.1.129's provider always runs `aforge exec --json -w <root>` with the binary resolved as `aforge` from PATH, so AFORGE_BIN and AGENTFIELD_AFORGE_COMMAND are accepted by the deployment surface but are no-ops. It also passes no `--timeout`, so aforge's own 15-minute wall applies before the SDK-side AGENTFIELD_HARNESS_TIMEOUT_SECONDS (default 1800s) can fire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BEHAVIOUR CHANGE, isolated here so it can be reviewed and reverted on its own. Before: `_openrouter_only_env()` returned False whenever ANTHROPIC_API_KEY was non-empty, so a deployment holding both keys resolved to `claude_code`. After: only an explicit SWE_DEFAULT_RUNTIME vetoes the OpenRouter choice, so both-keys deployments now resolve to `aforge` with the OpenRouter model defaults. Who this moves: anyone who has both ANTHROPIC_API_KEY and OPENROUTER_API_KEY set and has never set SWE_DEFAULT_RUNTIME. They silently switch harness and model family (sonnet/haiku -> openrouter/deepseek/deepseek-v4-flash-0731) and start spending on OpenRouter instead of Anthropic. The opt-out is `SWE_DEFAULT_RUNTIME=claude_code`. Reverting this commit alone restores the old precedence and leaves the rest of the aforge work intact — the only coupling is the docstrings and the two test cases changed here. Python and Go are changed together so the two swe-planner implementations stay in agreement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the gaps - README / .env.example / both agentfield-package.yaml manifests describe the aforge default, list `aforge` in SWE_DEFAULT_RUNTIME's value set, and declare AGENTFIELD_AFORGE_COMMAND. - README documents how the image gets AForge (download + checksum verification) and the two build args, including that AFORGE_VERSION is the cache key that actually pulls a newer binary. - Both README files carry the honest caveats rather than promising behaviour the pinned dependencies do not have: AFORGE_BIN / AGENTFIELD_AFORGE_COMMAND are no-ops on agentfield 0.1.129, and the Go node's aforge runtime needs AGENTFIELD_SDK_REF bumped past agentfield#905 before it can run at all. - docker-compose.go.yml passes AGENTFIELD_AFORGE_COMMAND through for both Go services, matching docker-compose.yml. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SWE-AF's CI had no docker job at all, so nothing caught the previous Dockerfile's reference to an image that is never published. The new job builds only `--target aforge` from both Dockerfiles — the download plus checksum verification — which is seconds of work and is the part that can actually rot (wrong version string, moved URL, changed checksums). The rest of the image (apt, npm, the Go build) is deliberately skipped. The coordinates are read out of the Dockerfile itself rather than duplicated here, so the job can never drift from what the image builds. The host is probed first: while agentfield.ai does not yet serve /downloads/aforge/<version>/, the job emits a notice and passes. Once the host is live, swap `--target aforge` for a full `docker build .` to make this the image-build gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AbirAbbas
force-pushed
the
agent/aforge-exec-default
branch
from
August 17, 2026 15:37
bbd5e11 to
572329a
Compare
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. Touches the ARG default in both the Python and Go Dockerfiles plus the matching docker build examples in the two READMEs. The AgentField SDK pins are deliberately left alone — they bump on their own release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v0.1.130 is the published release that carries agentfield#905, so the aforge harness provider honors AFORGE_BIN and AGENTFIELD_AFORGE_COMMAND and passes an explicit --timeout. Docker layer caching keys off the constraint string, so the floor itself has to move for an image rebuild to pick the new release up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Go node defaults to the aforge runtime, but harness.BuildProvider in the previously pinned July SDK snapshot knew only claude-code, codex, gemini and opencode — it returned `unknown harness provider: "aforge"`, so the default was unusable on this node. sdk/go/v0.1.130 is the first published submodule tag carrying agentfield#905, so go.mod now requires a real version instead of a pseudo-version, and go/Dockerfile, go/Makefile and the CI workflow pin the same release by its tag commit. The docs that described the old `replace`-directive layout and the "no submodule tags" workaround are updated to match: resolution goes through the module proxy, and the sparse SDK clone in the builder only records the pinned commit. No source changes were needed for the SDK jump — build, vet, gofmt and go test -race are clean on v0.1.130. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AbirAbbas
marked this pull request as ready for review
August 18, 2026 02:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Makes AForge the default SWE runtime, and — new in this revision — makes the PR
actually installable and buildable.
Three things in the original revision could not work:
FROM ghcr.io/agent-field/aforge-v2:chat-v2-exec. The aforge-v2 source repois private and no such image is published, so the build could never succeed
for anyone.
pointed at
agentfield @ git+...@bfd34426#subdirectory=sdk/python— a committhat exists only on the draft agentfield#905 branch, on no published index.
go/was untouched, so the Goimplementation of
swe-plannerstill resolved OpenRouter-only installs toopen_codewhile the shared README and manifests saidaforge.Distribution: download + verify, not a container image
The Dockerfile now has an
aforgefetch stage that downloads the releasedbinary and checksum-verifies it before it enters the image:
GET ${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${TARGETARCH}.gz/out/aforgeGET .../checksums.txt— its hashes are of the decompressed binaries —rewrite the matching
"<sha256> aforge-linux-<arch>"line to name the localfile,
sha256sum -cchmod +x+aforge --helpsmoke, so a corrupt or wrong-arch download failsthe build, not the deploy
COPY --from=aforge /out/aforge /usr/local/bin/aforgeas before. Both ARGs stayoverridable for mirrors/air-gapped registries, and
AFORGE_VERSIONis part ofthe layer cache key — bumping it is what pulls a newer AForge; a floating URL
alone would keep restoring the cached binary. Applied to
go/Dockerfiletoo,and
ca-certificatesis now explicit in the Python runtime stage (aforge speaksHTTPS to openrouter.ai).
Commits
feat(runtime): add aforge as a runtime and default it on OpenRouterfeat(go): mirror the aforge runtime and default on the Go nodego/internal/{runtimex,config,fast,orch,roles}build(docker): fetch and checksum-verify the released aforge binaryDockerfile,go/Dockerfilebuild(deps): re-pin agentfield to the published PyPI releasepyproject.toml,requirements*.txtfeat(runtime): prefer OpenRouter over Anthropic when both keys are setdocs(config): document the aforge default, the download pinning, and the gaps.env.example, manifests, composeci: build the aforge fetch stage when the download host is reachable.github/workflows/ci.ymlThe precedence change is its own commit
feat(runtime): prefer OpenRouter over Anthropic when both keys are setisdeliberately isolated so it can be reviewed — or reverted — on its own.
Before,
_openrouter_only_env()returned False wheneverANTHROPIC_API_KEYwasnon-empty, so a deployment holding both keys resolved to
claude_code.After, only an explicit
SWE_DEFAULT_RUNTIMEvetoes the OpenRouter choice.Anyone with both keys set and no
SWE_DEFAULT_RUNTIMEsilently switches harnessand model family (
sonnet/haiku→openrouter/deepseek/deepseek-v4-flash-0731)and starts spending on OpenRouter instead of Anthropic. Opt-out:
SWE_DEFAULT_RUNTIME=claude_code. Reverting that one commit restores the oldprecedence and leaves the rest of the aforge work intact.
Validation contract
Observable behaviours this change must exhibit:
OPENROUTER_API_KEYset and noSWE_DEFAULT_RUNTIME, both nodes resolvethe default runtime to
aforgeon the main, fast, issue and planning paths.ANTHROPIC_API_KEY, the default staysclaude_code.SWE_DEFAULT_RUNTIME, the default isaforge(the isolated precedence commit; reverting it returns
claude_code).SWE_DEFAULT_RUNTIME=open_codestill resolves toopen_code— the rollbackis configuration-only and OpenCode stays installed in the image.
aforgeruntime maps to a harness provider the pinned SDK actuallyregisters, and resolves to
openrouter/deepseek/deepseek-v4-flash-0731.docker build .produces an image with a workingaforgeonPATHwhose sha256 equals the release
checksums.txtentry foraforge-linux-amd64.pip install -e .resolves from PyPI, with no VCS dependency.swe-planneragree on every resolution above.How this was verified
Every gate the CI workflow runs, plus real container runs.
CI gates (workflow steps run literally, Python 3.12 / Go 1.23 semantics):
pip install -e ".[dev]"→ resolvesagentfield-0.1.129from PyPI.make check→ 1211 passed, 1 skipped +compileallclean.gofmt -l .clean ·go build ./...ok ·go vet ./...ok ·go test -race -count=1 ./...all packages ok.ruff check .— 789 findings, byte-identical to the count onorigin/main(pre-existing; CI does not run ruff).
Fetch stage, against a local mirror of the published layout:
Python image (
docker build ., then run):That hash is exactly the release
checksums.txtline foraforge-linux-amd64.aforge --helpprints the CLI usage;pip show agentfield→0.1.129.Real end-to-end exec inside the image (
OPENROUTER_API_KEYsupplied at runtime, tiny budget):
Default resolution inside the container (no test tree in the image, so the
config surface is exercised directly):
Go image (
docker build -f go/Dockerfile .): same verified aforge binary at/usr/local/bin/aforge(identical sha256),AGENTFIELD_AFORGE_COMMAND=exec,swe-planner/swe-fast/swe-proall present.Go default resolution vs. the pinned SDK (throwaway probe, not committed):
That last line is the Go-side gap, called out below.
CI job bash was executed under
bash --noprofile --norc -eo pipefailbeforecommitting: the probe correctly reports
available=falsefor the not-yet-livepublic host and
available=trueagainst a reachable one.Test images and containers were removed afterwards.
Known gaps — these are what keep the PR in draft
1. The download host does not serve this version yet. The Dockerfile
default
AFORGE_BASE_URL/AFORGE_VERSIONpoint athttps://agentfield.ai/downloads/aforge/v0.1.0/— aforge-v2's first semverrelease. The host is live and already serves the older
build-<sha>coordinate, but
v0.1.0/checksums.txtis still a 404 there. Everything abovewas verified against a local mirror of exactly that layout via
--build-arg AFORGE_BASE_URL=.... Nothing in the tree references anything butthe public URL.
2.
AFORGE_BINandAGENTFIELD_AFORGE_COMMANDare no-ops today. Onagentfield 0.1.129the aforge provider always runsaforge exec --json -w <root>and always resolves the binary asaforgefromPATH; neither env var is read. They are accepted by the deployment surface anddocumented as no-ops. It also passes no
--timeout, so aforge's own15-minute wall fires before the SDK-side
AGENTFIELD_HARNESS_TIMEOUT_SECONDS(default 1800s). Both start working with the release carrying agentfield#905.
Exact lines to bump when that release lands:
3. The Go node's
aforgedefault cannot run yet. The Go SDK pinned byAGENTFIELD_SDK_REFhas no aforge harness provider —harness.BuildProvider("aforge")returnsunknown harness provider: "aforge" (supported: claude-code, codex, gemini, opencode).The default is flipped for parity and the gap is documented at
DefaultRuntimeand in
go/README.md; until the pin moves, the Go node must be pointed atanother runtime with
SWE_DEFAULT_RUNTIME. Three lines move together:4. CI cannot build the full image yet. The new
aforge-fetch-stagejobprobes the download host and, while it is unreachable, emits a notice and
passes. It reads the coordinates out of the Dockerfile so it cannot drift from
what the image builds. Once the host is live, swap
--target aforgefor a fulldocker build .and this becomes the image-build gate.5. Not touched, on purpose.
sec-af/…is not applicable to this change.AForge already embeds swe-pro (
AFORGE_SWEPRO=1), so SWE-AF's own pro-enginevendoring (
go/bin/swe-pro-*,SWE_PRO_ENGINE) is deliberately left exactly asit is — collapsing the two is a separate decision.
Un-drafting checklist
https://agentfield.ai/downloads/aforge/v0.1.0/serves the gzippedbinaries +
checksums.txt(the release assets exist; publishing them tothe download host is the remaining step)
agentfield>=…lines above
AGENTFIELD_SDK_REF/go.modlocations to that release,so the Go node's aforge default resolves to a real provider
--target aforgeto a full image buildFollow-ups (out of scope here)
ENV HARNESS_MODEL=openrouter/moonshotai/kimi-k2.6forOpenCode's
small_modelinterpolation, andHARNESS_MODELsits in theSWE_DEFAULT_MODEL → AI_MODEL → HARNESS_MODELcascade. So inside the imageevery role resolves to kimi-k2.6, not the
openrouter/deepseek/deepseek-v4-flash-0731the docs advertise. Pre-existingon
main, unrelated to this change, but worth a dedicated fix.TARGETARCHand the release shipslinux/arm64, but nothing here builds or publishes an arm64 image.🤖 Generated with Claude Code