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..d87e47b 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@60b0aa76df604575429af60fb0ccffc9aaca7745#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..ed48525 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@60b0aa76df604575429af60fb0ccffc9aaca7745#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: