From acdd5c80680567361de3fb46acd2d00294b65045 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 10 Aug 2026 13:03:54 -0400 Subject: [PATCH 1/9] feat: add aforge harness option --- .env.example | 3 ++- Dockerfile | 2 +- README.md | 22 ++++++++++++++++++++-- agentfield-package.yaml | 4 +++- docker-compose.yml | 2 +- pyproject.toml | 5 ++++- src/sec_af/app.py | 1 + src/sec_af/config.py | 6 ++++++ tests/test_config.py | 5 +++++ 9 files changed, 43 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index cac03bf..2a0ff99 100644 --- a/.env.example +++ b/.env.example @@ -4,10 +4,11 @@ OPENROUTER_API_KEY= # Optional: API key required when the control plane enforces auth AGENTFIELD_API_KEY= -# Optional model/provider overrides +# Optional model/provider overrides (`aforge` uses the unreleased aforge-v2 CLI) HARNESS_PROVIDER=opencode HARNESS_MODEL=openrouter/moonshotai/kimi-k2.5 AI_MODEL=openrouter/moonshotai/kimi-k2.5 +# SEC_AF_AFORGE_BIN=/absolute/path/to/aforge # Optional: host path with repositories to scan/mirror into /workspaces # Example: SCAN_REPOS_PATH=../repos diff --git a/Dockerfile b/Dockerfile index 6b1c5a3..0740c42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ COPY pyproject.toml README.md ./ COPY src/ src/ RUN pip install --no-cache-dir --prefix=/install \ - "agentfield>=0.1.0" \ + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@72f3d00baf58efc4fec3f7ee16e69c7cb8f99ff9#subdirectory=sdk/python" \ "pydantic>=2.0" \ "httpx>=0.27" \ "python-dotenv>=1.0" && \ diff --git a/README.md b/README.md index 4ee88ef..24ac361 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ The pipeline adapts at runtime based on what it discovers. An AI gate examines r **6. Guided autonomy for coding agents** -SEC-AF runs on top of coding agents (Claude Code, OpenCode, Codex) via the AgentField harness. Rather than giving the agent a single massive prompt, each reasoner provides phase-aware guided autonomy: the agent receives a narrow task definition, a flat output schema (2-4 fields), and strategy-specific context. The agent has full autonomy within these boundaries — it can read files, trace code, and reason freely — but the harness constrains the _shape_ of its output. This prevents the common failure mode where autonomous agents go off-task or produce unstructured results. +SEC-AF runs on top of coding agents (Aforge, Claude Code, OpenCode, Codex) via the AgentField harness. Rather than giving the agent a single massive prompt, each reasoner provides phase-aware guided autonomy: the agent receives a narrow task definition, a flat output schema (2-4 fields), and strategy-specific context. The agent has full autonomy within these boundaries — it can read files, trace code, and reason freely — but the harness constrains the _shape_ of its output. This prevents the common failure mode where autonomous agents go off-task or produce unstructured results. **7. Composable reasoner DAG with full observability** @@ -477,13 +477,31 @@ jobs: | `SEC_AF_MAX_TURNS` | No | `50` | Max harness turns per call | | `AGENTFIELD_API_KEY` | No | unset | API key for secured environments | | `SEC_AF_WORKSPACES_DIR` | No | `/workspaces` | Directory for cloned repos (falls back to `~/.sec-af/workspaces` if not writable) | -| `HARNESS_PROVIDER` | No | `opencode` | Harness backend provider | +| `HARNESS_PROVIDER` | No | `opencode` | Harness backend provider (`aforge`, `claude-code`, `codex`, `gemini`, or `opencode`) | +| `SEC_AF_AFORGE_BIN` | No | `aforge` | Path to an aforge-v2 binary for the `aforge` provider | | `SEC_AF_AI_MAX_RETRIES` | No | `3` | Retry count for model calls | ## Development Setup +### Benchmark with unreleased aforge-v2 + +```bash +cd /path/to/aforge-v2 +go build -o /absolute/path/to/bin/aforge ./cmd/aforge + +export OPENROUTER_API_KEY=sk-or-v1-... +export HARNESS_PROVIDER=aforge +export SEC_AF_AFORGE_BIN=/absolute/path/to/bin/aforge +python -m sec_af.app +``` + +This uses the same SEC-AF prompts and schemas as OpenCode; only the AgentField +harness provider changes. The draft pins the exact AgentField harness commit +used for benchmarking; replace that pin with the released SDK before merging. +The current Docker image does not bundle the unreleased Aforge binary. + ```bash python -m venv .venv && source .venv/bin/activate pip install -e .[dev] diff --git a/agentfield-package.yaml b/agentfield-package.yaml index c677a4d..d3b506b 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -27,8 +27,10 @@ user_environment: type: secret scope: global - name: HARNESS_PROVIDER - description: Coding-agent harness provider + description: Coding-agent harness provider (aforge | claude-code | codex | gemini | opencode) default: opencode + - name: SEC_AF_AFORGE_BIN + description: Optional path to the aforge-v2 binary (defaults to aforge on PATH) - name: HARNESS_MODEL description: Model the harness uses default: openrouter/moonshotai/kimi-k2.5 diff --git a/docker-compose.yml b/docker-compose.yml index b2c6ba2..2a7903a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: - AGENTFIELD_API_KEY=${AGENTFIELD_API_KEY:-} - PORT=8003 - AGENT_CALLBACK_URL=http://sec-af:8003 - - HARNESS_PROVIDER=opencode + - HARNESS_PROVIDER=${HARNESS_PROVIDER:-opencode} - HARNESS_MODEL=${HARNESS_MODEL:-openrouter/moonshotai/kimi-k2.5} - AI_MODEL=${AI_MODEL:-openrouter/moonshotai/kimi-k2.5} - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} diff --git a/pyproject.toml b/pyproject.toml index 747af56..75bd8de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" requires-python = ">=3.11" authors = [{ name = "AgentField", email = "hello@agentfield.dev" }] dependencies = [ - "agentfield>=0.1.0", + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@72f3d00baf58efc4fec3f7ee16e69c7cb8f99ff9#subdirectory=sdk/python", "pydantic>=2.0", "httpx>=0.27", ] @@ -31,6 +31,9 @@ sec-af = "sec_af.app:main" [tool.hatch.build.targets.wheel] packages = ["src/sec_af"] +[tool.hatch.metadata] +allow-direct-references = true + [tool.ruff] target-version = "py311" line-length = 120 diff --git a/src/sec_af/app.py b/src/sec_af/app.py index a6c574a..3c7c895 100644 --- a/src/sec_af/app.py +++ b/src/sec_af/app.py @@ -45,6 +45,7 @@ max_turns=_ai_config.max_turns, env=_ai_config.provider_env(), opencode_bin=_ai_config.opencode_bin, + aforge_bin=_ai_config.aforge_bin, permission_mode="auto", ), ai_config=AIConfig( diff --git a/src/sec_af/config.py b/src/sec_af/config.py index 382798c..5a638a9 100644 --- a/src/sec_af/config.py +++ b/src/sec_af/config.py @@ -95,6 +95,12 @@ class AIIntegrationConfig(BaseModel): ) max_backoff_seconds: float = Field(default_factory=lambda: float(os.getenv("SEC_AF_AI_MAX_BACKOFF_SECONDS", "8.0"))) opencode_bin: str = Field(default_factory=lambda: os.getenv("SEC_AF_OPENCODE_BIN", "opencode")) + aforge_bin: str = Field( + default_factory=lambda: os.getenv( + "SEC_AF_AFORGE_BIN", + os.getenv("AFORGE_BIN", "aforge"), + ) + ) opencode_server: str | None = Field( default_factory=lambda: os.getenv("SEC_AF_OPENCODE_SERVER", os.getenv("OPENCODE_SERVER")), ) diff --git a/tests/test_config.py b/tests/test_config.py index adf903d..dff24a0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -62,6 +62,7 @@ def test_ai_integration_config_uses_sec_af_env_precedence(monkeypatch: pytest.Mo monkeypatch.setenv("SEC_AF_AI_INITIAL_BACKOFF_SECONDS", "1.5") monkeypatch.setenv("SEC_AF_AI_MAX_BACKOFF_SECONDS", "12") monkeypatch.setenv("SEC_AF_OPENCODE_BIN", "/usr/local/bin/opencode") + monkeypatch.setenv("SEC_AF_AFORGE_BIN", "/usr/local/bin/aforge") config = AIIntegrationConfig.from_env() @@ -73,6 +74,7 @@ def test_ai_integration_config_uses_sec_af_env_precedence(monkeypatch: pytest.Mo assert config.initial_backoff_seconds == 1.5 assert config.max_backoff_seconds == 12 assert config.opencode_bin == "/usr/local/bin/opencode" + assert config.aforge_bin == "/usr/local/bin/aforge" def test_ai_integration_config_falls_back_to_harness_and_defaults(monkeypatch: pytest.MonkeyPatch) -> None: @@ -88,6 +90,8 @@ def test_ai_integration_config_falls_back_to_harness_and_defaults(monkeypatch: p "SEC_AF_AI_INITIAL_BACKOFF_SECONDS", "SEC_AF_AI_MAX_BACKOFF_SECONDS", "SEC_AF_OPENCODE_BIN", + "SEC_AF_AFORGE_BIN", + "AFORGE_BIN", ): monkeypatch.delenv(key, raising=False) @@ -101,6 +105,7 @@ def test_ai_integration_config_falls_back_to_harness_and_defaults(monkeypatch: p assert config.initial_backoff_seconds == 2.0 assert config.max_backoff_seconds == 8.0 assert config.opencode_bin == "opencode" + assert config.aforge_bin == "aforge" def test_provider_env_only_includes_present_keys(monkeypatch: pytest.MonkeyPatch) -> None: From ae0b656534c53d074e428166a814cc8343de1259 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 14 Aug 2026 23:38:38 -0400 Subject: [PATCH 2/9] feat: make aforge exec the default harness --- .env.example | 5 +++-- Dockerfile | 10 ++++++++-- README.md | 18 +++++++----------- agentfield-package.yaml | 5 ++++- docker-compose.yml | 3 ++- pyproject.toml | 2 +- src/sec_af/config.py | 5 +++-- tests/test_config.py | 3 ++- 8 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index 2a0ff99..e70fd90 100644 --- a/.env.example +++ b/.env.example @@ -4,8 +4,9 @@ OPENROUTER_API_KEY= # Optional: API key required when the control plane enforces auth AGENTFIELD_API_KEY= -# Optional model/provider overrides (`aforge` uses the unreleased aforge-v2 CLI) -HARNESS_PROVIDER=opencode +# AForge exec is the default. Set HARNESS_PROVIDER=opencode to roll back. +HARNESS_PROVIDER=aforge +AGENTFIELD_AFORGE_COMMAND=exec HARNESS_MODEL=openrouter/moonshotai/kimi-k2.5 AI_MODEL=openrouter/moonshotai/kimi-k2.5 # SEC_AF_AFORGE_BIN=/absolute/path/to/aforge diff --git a/Dockerfile b/Dockerfile index 0740c42..0349102 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,7 @@ +ARG AFORGE_IMAGE=ghcr.io/agent-field/aforge-v2:chat-v2-exec +FROM ${AFORGE_IMAGE} AS aforge + + FROM python:3.11-slim AS builder ENV PYTHONDONTWRITEBYTECODE=1 \ @@ -14,7 +18,7 @@ COPY pyproject.toml README.md ./ COPY src/ src/ RUN pip install --no-cache-dir --prefix=/install \ - "agentfield @ git+https://github.com/Agent-Field/agentfield.git@72f3d00baf58efc4fec3f7ee16e69c7cb8f99ff9#subdirectory=sdk/python" \ + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python" \ "pydantic>=2.0" \ "httpx>=0.27" \ "python-dotenv>=1.0" && \ @@ -25,7 +29,8 @@ FROM python:3.11-slim AS runtime ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ - HARNESS_PROVIDER=opencode \ + HARNESS_PROVIDER=aforge \ + AGENTFIELD_AFORGE_COMMAND=exec \ HARNESS_MODEL=openrouter/minimax/minimax-m2.5 \ AI_MODEL=openrouter/minimax/minimax-m2.5 \ PORT=8080 \ @@ -53,6 +58,7 @@ RUN mkdir -p /home/secaf/.config/opencode && \ chown -R secaf:secaf /home/secaf/.config COPY --from=builder /install /usr/local +COPY --from=aforge /aforge /usr/local/bin/aforge COPY src/ /app/src/ USER secaf diff --git a/README.md b/README.md index 24ac361..45ab195 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ The pipeline adapts at runtime based on what it discovers. An AI gate examines r **6. Guided autonomy for coding agents** -SEC-AF runs on top of coding agents (Aforge, Claude Code, OpenCode, Codex) via the AgentField harness. Rather than giving the agent a single massive prompt, each reasoner provides phase-aware guided autonomy: the agent receives a narrow task definition, a flat output schema (2-4 fields), and strategy-specific context. The agent has full autonomy within these boundaries — it can read files, trace code, and reason freely — but the harness constrains the _shape_ of its output. This prevents the common failure mode where autonomous agents go off-task or produce unstructured results. +SEC-AF runs on top of coding agents (AForge, Claude Code, OpenCode, Codex) via the AgentField harness. Rather than giving the agent a single massive prompt, each reasoner provides phase-aware guided autonomy: the agent receives a narrow task definition, a flat output schema (2-4 fields), and strategy-specific context. The agent has full autonomy within these boundaries — it can read files, trace code, and reason freely — but the harness constrains the _shape_ of its output. This prevents the common failure mode where autonomous agents go off-task or produce unstructured results. **7. Composable reasoner DAG with full observability** @@ -477,7 +477,8 @@ jobs: | `SEC_AF_MAX_TURNS` | No | `50` | Max harness turns per call | | `AGENTFIELD_API_KEY` | No | unset | API key for secured environments | | `SEC_AF_WORKSPACES_DIR` | No | `/workspaces` | Directory for cloned repos (falls back to `~/.sec-af/workspaces` if not writable) | -| `HARNESS_PROVIDER` | No | `opencode` | Harness backend provider (`aforge`, `claude-code`, `codex`, `gemini`, or `opencode`) | +| `HARNESS_PROVIDER` | No | `aforge` | Harness backend provider; set `opencode` for rollback | +| `AGENTFIELD_AFORGE_COMMAND` | No | `exec` | AForge headless command (`do` remains an explicit override) | | `SEC_AF_AFORGE_BIN` | No | `aforge` | Path to an aforge-v2 binary for the `aforge` provider | | `SEC_AF_AI_MAX_RETRIES` | No | `3` | Retry count for model calls | @@ -485,22 +486,17 @@ jobs: ## Development Setup -### Benchmark with unreleased aforge-v2 +### Harness selection ```bash -cd /path/to/aforge-v2 -go build -o /absolute/path/to/bin/aforge ./cmd/aforge - export OPENROUTER_API_KEY=sk-or-v1-... export HARNESS_PROVIDER=aforge -export SEC_AF_AFORGE_BIN=/absolute/path/to/bin/aforge +export AGENTFIELD_AFORGE_COMMAND=exec python -m sec_af.app ``` -This uses the same SEC-AF prompts and schemas as OpenCode; only the AgentField -harness provider changes. The draft pins the exact AgentField harness commit -used for benchmarking; replace that pin with the released SDK before merging. -The current Docker image does not bundle the unreleased Aforge binary. +The container includes AForge and uses its `exec` interface by default. To +temporarily roll back without changing code, set `HARNESS_PROVIDER=opencode`. ```bash python -m venv .venv && source .venv/bin/activate diff --git a/agentfield-package.yaml b/agentfield-package.yaml index d3b506b..755e3cd 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -28,7 +28,10 @@ user_environment: scope: global - name: HARNESS_PROVIDER description: Coding-agent harness provider (aforge | claude-code | codex | gemini | opencode) - default: opencode + default: aforge + - name: AGENTFIELD_AFORGE_COMMAND + description: AForge headless command (exec is the product default; do remains available) + default: exec - name: SEC_AF_AFORGE_BIN description: Optional path to the aforge-v2 binary (defaults to aforge on PATH) - name: HARNESS_MODEL diff --git a/docker-compose.yml b/docker-compose.yml index 2a7903a..dc987db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,8 @@ services: - AGENTFIELD_API_KEY=${AGENTFIELD_API_KEY:-} - PORT=8003 - AGENT_CALLBACK_URL=http://sec-af:8003 - - HARNESS_PROVIDER=${HARNESS_PROVIDER:-opencode} + - HARNESS_PROVIDER=${HARNESS_PROVIDER:-aforge} + - AGENTFIELD_AFORGE_COMMAND=${AGENTFIELD_AFORGE_COMMAND:-exec} - HARNESS_MODEL=${HARNESS_MODEL:-openrouter/moonshotai/kimi-k2.5} - AI_MODEL=${AI_MODEL:-openrouter/moonshotai/kimi-k2.5} - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} diff --git a/pyproject.toml b/pyproject.toml index 75bd8de..7344fb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" requires-python = ">=3.11" authors = [{ name = "AgentField", email = "hello@agentfield.dev" }] dependencies = [ - "agentfield @ git+https://github.com/Agent-Field/agentfield.git@72f3d00baf58efc4fec3f7ee16e69c7cb8f99ff9#subdirectory=sdk/python", + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python", "pydantic>=2.0", "httpx>=0.27", ] diff --git a/src/sec_af/config.py b/src/sec_af/config.py index 5a638a9..3f56f53 100644 --- a/src/sec_af/config.py +++ b/src/sec_af/config.py @@ -47,7 +47,7 @@ class AuditConfig(BaseModel): exclude_paths: list[str] = Field( default_factory=lambda: ["tests/", "vendor/", "node_modules/", ".git/"], ) - provider: str = "opencode" + provider: str = "aforge" budget: BudgetConfig = Field(default_factory=BudgetConfig) @classmethod @@ -74,7 +74,7 @@ def from_input(cls, audit_input: AuditInput, repo_path: str) -> "AuditConfig": class AIIntegrationConfig(BaseModel): provider: str = Field( - default_factory=lambda: os.getenv("SEC_AF_PROVIDER", os.getenv("HARNESS_PROVIDER", "opencode")) + default_factory=lambda: os.getenv("SEC_AF_PROVIDER", os.getenv("HARNESS_PROVIDER", "aforge")) ) harness_model: str = Field( default_factory=lambda: os.getenv( @@ -119,6 +119,7 @@ def provider_env(self) -> dict[str, str]: "GH_TOKEN", ) env: dict[str, str] = {key: value for key in env_keys if (value := os.getenv(key))} + env["AGENTFIELD_AFORGE_COMMAND"] = os.getenv("AGENTFIELD_AFORGE_COMMAND", "exec") xdg = os.getenv("XDG_DATA_HOME") or os.path.join(tempfile.gettempdir(), "opencode-shared-data") os.makedirs(xdg, exist_ok=True) env["XDG_DATA_HOME"] = xdg diff --git a/tests/test_config.py b/tests/test_config.py index dff24a0..57783c4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -97,7 +97,7 @@ def test_ai_integration_config_falls_back_to_harness_and_defaults(monkeypatch: p config = AIIntegrationConfig.from_env() - assert config.provider == "opencode" + assert config.provider == "aforge" assert config.harness_model == "minimax/minimax-m2.5" assert config.ai_model == "minimax/minimax-m2.5" assert config.max_turns == 50 @@ -106,6 +106,7 @@ def test_ai_integration_config_falls_back_to_harness_and_defaults(monkeypatch: p assert config.max_backoff_seconds == 8.0 assert config.opencode_bin == "opencode" assert config.aforge_bin == "aforge" + assert config.provider_env()["AGENTFIELD_AFORGE_COMMAND"] == "exec" def test_provider_env_only_includes_present_keys(monkeypatch: pytest.MonkeyPatch) -> None: From 86701a2234321fc4ce432a5895a036dafc166277 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 10:57:02 -0400 Subject: [PATCH 3/9] build: fetch and verify the released AForge CLI at image build time The image previously pulled AForge out of a `ghcr.io/agent-field/aforge-v2` container image, which made the build depend on a private registry tag that does not exist yet. Replace that with a fetch stage that downloads the published, gzipped release binary for the build's TARGETARCH, decompresses it, and verifies the *decompressed* SHA-256 against the release `checksums.txt` before it is copied into the runtime image. A tampered or truncated download fails the build instead of shipping. `AFORGE_BASE_URL` and `AFORGE_VERSION` stay overridable as build args (and are plumbed through docker-compose) so CI or a local mirror can point elsewhere without editing the Dockerfile. Co-Authored-By: Claude Fable 5 --- Dockerfile | 39 ++++++++++++++++++++++++++++++++++++--- docker-compose.yml | 3 +++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0349102..a9248c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,38 @@ -ARG AFORGE_IMAGE=ghcr.io/agent-field/aforge-v2:chat-v2-exec -FROM ${AFORGE_IMAGE} AS aforge +# AForge CLI is fetched as a released, checksum-verified binary rather than +# copied out of a container image, so the build depends only on the public +# download host. Both ARGs are overridable (e.g. to point at a staging mirror). +ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge +ARG AFORGE_VERSION=build-9b3ff482de3f + +FROM debian:bookworm-slim AS aforge + +ARG AFORGE_BASE_URL +ARG AFORGE_VERSION +# Provided automatically by BuildKit; defaults to amd64 for legacy builders. +ARG TARGETARCH + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /out + +# Download the gzipped release binary, decompress it, and verify the +# *decompressed* SHA-256 against the release checksums.txt before use. +RUN set -eux; \ + arch="${TARGETARCH:-amd64}"; \ + curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${arch}.gz" -o aforge.gz; \ + gunzip -c aforge.gz > aforge; \ + rm aforge.gz; \ + curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/checksums.txt" -o checksums.txt; \ + tr -d '\r' < checksums.txt \ + | grep " aforge-linux-${arch}$" \ + | sed 's/ aforge-linux-.*/ aforge/' > aforge.sha256; \ + test -s aforge.sha256; \ + sha256sum -c aforge.sha256; \ + rm checksums.txt aforge.sha256; \ + chmod +x aforge FROM python:3.11-slim AS builder @@ -58,7 +91,7 @@ RUN mkdir -p /home/secaf/.config/opencode && \ chown -R secaf:secaf /home/secaf/.config COPY --from=builder /install /usr/local -COPY --from=aforge /aforge /usr/local/bin/aforge +COPY --from=aforge /out/aforge /usr/local/bin/aforge COPY src/ /app/src/ USER secaf diff --git a/docker-compose.yml b/docker-compose.yml index dc987db..91eaee0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,9 @@ services: build: context: . dockerfile: Dockerfile + args: + AFORGE_BASE_URL: ${AFORGE_BASE_URL:-https://agentfield.ai/downloads/aforge} + AFORGE_VERSION: ${AFORGE_VERSION:-build-9b3ff482de3f} ports: - "8003:8003" environment: From c592983b404f29ea1347560a6078349fb9147070 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 10:57:14 -0400 Subject: [PATCH 4/9] deps: pin agentfield to the released 0.1.129 instead of a git SHA The branch depended on an unmerged agentfield commit via a direct git reference, so `pip install sec-af` only worked with VCS access and `allow-direct-references` turned on. agentfield 0.1.129 is on PyPI and already ships the `aforge` harness provider plus the `aforge_bin` field on `HarnessConfig`, which is everything this repo uses today. Pin the floor there and drop the now-unnecessary `[tool.hatch.metadata] allow-direct-references`. Co-Authored-By: Claude Fable 5 --- Dockerfile | 2 +- pyproject.toml | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index a9248c5..eceec5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ COPY pyproject.toml README.md ./ COPY src/ src/ RUN pip install --no-cache-dir --prefix=/install \ - "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python" \ + "agentfield>=0.1.129" \ "pydantic>=2.0" \ "httpx>=0.27" \ "python-dotenv>=1.0" && \ diff --git a/pyproject.toml b/pyproject.toml index 7344fb0..e36c55b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" requires-python = ">=3.11" authors = [{ name = "AgentField", email = "hello@agentfield.dev" }] dependencies = [ - "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python", + "agentfield>=0.1.129", "pydantic>=2.0", "httpx>=0.27", ] @@ -31,9 +31,6 @@ sec-af = "sec_af.app:main" [tool.hatch.build.targets.wheel] packages = ["src/sec_af"] -[tool.hatch.metadata] -allow-direct-references = true - [tool.ruff] target-version = "py311" line-length = 120 From 77bd462a58059be4f51c62c3640405d14ecb75fd Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 10:57:14 -0400 Subject: [PATCH 5/9] test: pin the aforge default and the SDK surface it depends on Two behaviours had no coverage: - `AuditConfig` (the orchestrator's runtime config) defaults to the `aforge` provider, not just `AIIntegrationConfig`. - The pinned agentfield floor actually accepts the harness settings `app.py` sends it. `aforge_bin` only exists on `HarnessConfig` from 0.1.129 onward, so lowering the floor now fails a test instead of failing at agent startup. Co-Authored-By: Claude Fable 5 --- tests/test_config.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index 57783c4..e241f78 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,6 +4,7 @@ from typing import Any, cast import pytest +from agentfield import HarnessConfig from sec_af.config import AIIntegrationConfig, AuditConfig, BudgetConfig, DepthProfile from sec_af.schemas.input import AuditInput @@ -123,3 +124,29 @@ def test_provider_env_only_includes_present_keys(monkeypatch: pytest.MonkeyPatch assert env["GITHUB_TOKEN"] == "test-gh" assert "OPENROUTER_API_KEY" not in env assert "XDG_DATA_HOME" in env + + +def test_audit_config_defaults_to_aforge_provider(sample_audit_input: AuditInput) -> None: + config = AuditConfig.from_input(sample_audit_input, repo_path="/tmp/sec-af-repo") + + assert config.provider == "aforge" + + +def test_pinned_agentfield_sdk_exposes_the_aforge_surface(monkeypatch: pytest.MonkeyPatch) -> None: + """The pinned AgentField SDK must accept the aforge harness settings app.py sends it.""" + for key in ("SEC_AF_PROVIDER", "HARNESS_PROVIDER", "SEC_AF_AFORGE_BIN", "AFORGE_BIN"): + monkeypatch.delenv(key, raising=False) + + config = AIIntegrationConfig.from_env() + harness_config = HarnessConfig( + provider=config.provider, + model=config.harness_model, + max_turns=config.max_turns, + env=config.provider_env(), + opencode_bin=config.opencode_bin, + aforge_bin=config.aforge_bin, + permission_mode="auto", + ) + + assert harness_config.provider == "aforge" + assert harness_config.aforge_bin == "aforge" From fc5c54e66ffe83f44a6a0933dd6e9f27dc5f988b Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 10:57:19 -0400 Subject: [PATCH 6/9] docs: describe the AForge build args and mark AGENTFIELD_AFORGE_COMMAND reserved The docs claimed `AGENTFIELD_AFORGE_COMMAND` selects the AForge subcommand and that `do` is an available override. It does not: agentfield 0.1.129's aforge provider always runs `aforge exec`, so the variable is forwarded to the harness environment and ignored. Label it reserved everywhere it is documented rather than promise behaviour that does not exist yet. Also document `AFORGE_BASE_URL` / `AFORGE_VERSION` and state that the image installs a checksum-verified AForge at /usr/local/bin/aforge. Co-Authored-By: Claude Fable 5 --- .env.example | 6 ++++++ README.md | 29 ++++++++++++++++++++++++----- agentfield-package.yaml | 4 ++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index e70fd90..bf3808b 100644 --- a/.env.example +++ b/.env.example @@ -6,11 +6,17 @@ AGENTFIELD_API_KEY= # AForge exec is the default. Set HARNESS_PROVIDER=opencode to roll back. HARNESS_PROVIDER=aforge +# Reserved: forwarded to the harness environment. The AgentField SDK always +# runs `aforge exec` today, so this is a no-op until it exposes a selector. AGENTFIELD_AFORGE_COMMAND=exec HARNESS_MODEL=openrouter/moonshotai/kimi-k2.5 AI_MODEL=openrouter/moonshotai/kimi-k2.5 # SEC_AF_AFORGE_BIN=/absolute/path/to/aforge +# Build-time only: where `docker compose build` fetches the AForge CLI from. +# AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge +# AFORGE_VERSION=build-9b3ff482de3f + # Optional: host path with repositories to scan/mirror into /workspaces # Example: SCAN_REPOS_PATH=../repos SCAN_REPOS_PATH=./workspaces diff --git a/README.md b/README.md index 45ab195..dc6dd16 100644 --- a/README.md +++ b/README.md @@ -478,12 +478,30 @@ jobs: | `AGENTFIELD_API_KEY` | No | unset | API key for secured environments | | `SEC_AF_WORKSPACES_DIR` | No | `/workspaces` | Directory for cloned repos (falls back to `~/.sec-af/workspaces` if not writable) | | `HARNESS_PROVIDER` | No | `aforge` | Harness backend provider; set `opencode` for rollback | -| `AGENTFIELD_AFORGE_COMMAND` | No | `exec` | AForge headless command (`do` remains an explicit override) | -| `SEC_AF_AFORGE_BIN` | No | `aforge` | Path to an aforge-v2 binary for the `aforge` provider | +| `AGENTFIELD_AFORGE_COMMAND` | No | `exec` | Reserved. Forwarded to the harness environment; the AgentField SDK always runs `aforge exec` today, so this is a no-op until the SDK exposes a command selector | +| `SEC_AF_AFORGE_BIN` | No | `aforge` | Path to an AForge binary for the `aforge` provider (also honours `AFORGE_BIN`) | | `SEC_AF_AI_MAX_RETRIES` | No | `3` | Retry count for model calls | +
+Build arguments + +The image downloads the released AForge CLI at build time and verifies its +SHA-256 against the release `checksums.txt` before installing it. + +| Build arg | Default | Description | +|---|---|---| +| `AFORGE_BASE_URL` | `https://agentfield.ai/downloads/aforge` | Root of the AForge download host | +| `AFORGE_VERSION` | `build-9b3ff482de3f` | Released AForge build to install | + +```bash +docker build -t sec-af . +docker build --build-arg AFORGE_VERSION=build-xxxxxxxxxxxx -t sec-af . +``` + +
+ ## Development Setup ### Harness selection @@ -491,12 +509,13 @@ jobs: ```bash export OPENROUTER_API_KEY=sk-or-v1-... export HARNESS_PROVIDER=aforge -export AGENTFIELD_AFORGE_COMMAND=exec python -m sec_af.app ``` -The container includes AForge and uses its `exec` interface by default. To -temporarily roll back without changing code, set `HARNESS_PROVIDER=opencode`. +The container ships the AForge CLI at `/usr/local/bin/aforge` and drives it +through `aforge exec` by default. To temporarily roll back without changing +code, set `HARNESS_PROVIDER=opencode`. Running outside the container requires +an `aforge` binary on `PATH` (or `SEC_AF_AFORGE_BIN` pointing at one). ```bash python -m venv .venv && source .venv/bin/activate diff --git a/agentfield-package.yaml b/agentfield-package.yaml index 755e3cd..7af1ba3 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -30,10 +30,10 @@ user_environment: description: Coding-agent harness provider (aforge | claude-code | codex | gemini | opencode) default: aforge - name: AGENTFIELD_AFORGE_COMMAND - description: AForge headless command (exec is the product default; do remains available) + description: Reserved. AForge headless command; the SDK always runs `aforge exec` today, so this is a no-op until it exposes a selector default: exec - name: SEC_AF_AFORGE_BIN - description: Optional path to the aforge-v2 binary (defaults to aforge on PATH) + description: Optional path to the AForge binary (defaults to aforge on PATH) - name: HARNESS_MODEL description: Model the harness uses default: openrouter/moonshotai/kimi-k2.5 From 6d21365ec2bb8c99b7bf323e63370c8634f80672 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 13:11:16 -0400 Subject: [PATCH 7/9] chore: pin aforge to v0.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aforge-v2 cut its first semver release (v0.1.0), so the AFORGE_VERSION default moves off the build- 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. Moves all four places the coordinate is written: the Dockerfile ARG, the docker-compose build arg fallback, the .env.example hint, and the README build-arg table. The README's override example loses its build- placeholder shape at the same time, since versions are tags now. The AgentField SDK pin is deliberately left alone — it bumps on its own release. Co-Authored-By: Claude Fable 5 --- .env.example | 2 +- Dockerfile | 2 +- README.md | 4 ++-- docker-compose.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index bf3808b..aff1589 100644 --- a/.env.example +++ b/.env.example @@ -15,7 +15,7 @@ AI_MODEL=openrouter/moonshotai/kimi-k2.5 # Build-time only: where `docker compose build` fetches the AForge CLI from. # AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge -# AFORGE_VERSION=build-9b3ff482de3f +# AFORGE_VERSION=v0.1.0 # Optional: host path with repositories to scan/mirror into /workspaces # Example: SCAN_REPOS_PATH=../repos diff --git a/Dockerfile b/Dockerfile index eceec5c..22db69d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # copied out of a container image, so the build depends only on the public # download host. Both ARGs are overridable (e.g. to point at a staging mirror). ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge -ARG AFORGE_VERSION=build-9b3ff482de3f +ARG AFORGE_VERSION=v0.1.0 FROM debian:bookworm-slim AS aforge diff --git a/README.md b/README.md index dc6dd16..6b506e8 100644 --- a/README.md +++ b/README.md @@ -493,11 +493,11 @@ SHA-256 against the release `checksums.txt` before installing it. | Build arg | Default | Description | |---|---|---| | `AFORGE_BASE_URL` | `https://agentfield.ai/downloads/aforge` | Root of the AForge download host | -| `AFORGE_VERSION` | `build-9b3ff482de3f` | Released AForge build to install | +| `AFORGE_VERSION` | `v0.1.0` | Released AForge version to install | ```bash docker build -t sec-af . -docker build --build-arg AFORGE_VERSION=build-xxxxxxxxxxxx -t sec-af . +docker build --build-arg AFORGE_VERSION=vX.Y.Z -t sec-af . ``` diff --git a/docker-compose.yml b/docker-compose.yml index 91eaee0..6a48977 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: dockerfile: Dockerfile args: AFORGE_BASE_URL: ${AFORGE_BASE_URL:-https://agentfield.ai/downloads/aforge} - AFORGE_VERSION: ${AFORGE_VERSION:-build-9b3ff482de3f} + AFORGE_VERSION: ${AFORGE_VERSION:-v0.1.0} ports: - "8003:8003" environment: From a54f889c9258d75ecf52c7d44b969ccb55c101e9 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 21:40:35 -0400 Subject: [PATCH 8/9] chore: pin agentfield>=0.1.130 (aforge default release) agentfield 0.1.130 is the first release that ships the aforge harness provider as the default, so the runtime pin moves up with it. Co-Authored-By: Claude Fable 5 --- Dockerfile | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 22db69d..c08e470 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ COPY pyproject.toml README.md ./ COPY src/ src/ RUN pip install --no-cache-dir --prefix=/install \ - "agentfield>=0.1.129" \ + "agentfield>=0.1.130" \ "pydantic>=2.0" \ "httpx>=0.27" \ "python-dotenv>=1.0" && \ diff --git a/pyproject.toml b/pyproject.toml index e36c55b..ae35577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" requires-python = ">=3.11" authors = [{ name = "AgentField", email = "hello@agentfield.dev" }] dependencies = [ - "agentfield>=0.1.129", + "agentfield>=0.1.130", "pydantic>=2.0", "httpx>=0.27", ] From 458ddec1b02b0eb0ab2e5a16835d62fa13eb99ef Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 22:12:42 -0400 Subject: [PATCH 9/9] docs: AGENTFIELD_AFORGE_COMMAND is a live exec|do selector on agentfield>=0.1.130 The "reserved / no-op" wording was written against 0.1.129, whose aforge provider hardcoded `exec`. 0.1.130 reads AGENTFIELD_AFORGE_COMMAND (default `exec`, `do` opts into the routed workflow), so describe the real behaviour. Co-Authored-By: Claude Fable 5 --- .env.example | 4 ++-- README.md | 2 +- agentfield-package.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index aff1589..30c59ea 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,8 @@ AGENTFIELD_API_KEY= # AForge exec is the default. Set HARNESS_PROVIDER=opencode to roll back. HARNESS_PROVIDER=aforge -# Reserved: forwarded to the harness environment. The AgentField SDK always -# runs `aforge exec` today, so this is a no-op until it exposes a selector. +# AForge headless command the SDK runs: `exec` (default) or `do`. +# Read by agentfield>=0.1.130; any other value fails the harness call. AGENTFIELD_AFORGE_COMMAND=exec HARNESS_MODEL=openrouter/moonshotai/kimi-k2.5 AI_MODEL=openrouter/moonshotai/kimi-k2.5 diff --git a/README.md b/README.md index 6b506e8..6450afb 100644 --- a/README.md +++ b/README.md @@ -478,7 +478,7 @@ jobs: | `AGENTFIELD_API_KEY` | No | unset | API key for secured environments | | `SEC_AF_WORKSPACES_DIR` | No | `/workspaces` | Directory for cloned repos (falls back to `~/.sec-af/workspaces` if not writable) | | `HARNESS_PROVIDER` | No | `aforge` | Harness backend provider; set `opencode` for rollback | -| `AGENTFIELD_AFORGE_COMMAND` | No | `exec` | Reserved. Forwarded to the harness environment; the AgentField SDK always runs `aforge exec` today, so this is a no-op until the SDK exposes a command selector | +| `AGENTFIELD_AFORGE_COMMAND` | No | `exec` | AForge headless command the SDK runs: `exec` (default) or `do`. Read by agentfield>=0.1.130 | | `SEC_AF_AFORGE_BIN` | No | `aforge` | Path to an AForge binary for the `aforge` provider (also honours `AFORGE_BIN`) | | `SEC_AF_AI_MAX_RETRIES` | No | `3` | Retry count for model calls | diff --git a/agentfield-package.yaml b/agentfield-package.yaml index 7af1ba3..fae8a33 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -30,7 +30,7 @@ user_environment: description: Coding-agent harness provider (aforge | claude-code | codex | gemini | opencode) default: aforge - name: AGENTFIELD_AFORGE_COMMAND - description: Reserved. AForge headless command; the SDK always runs `aforge exec` today, so this is a no-op until it exposes a selector + description: AForge headless command the SDK runs (`exec` or `do`); agentfield>=0.1.130 reads it, default `exec` default: exec - name: SEC_AF_AFORGE_BIN description: Optional path to the AForge binary (defaults to aforge on PATH)