diff --git a/.env.example b/.env.example index 41bd575..31a55cf 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,11 @@ OPENROUTER_API_KEY=sk-or-... AGENTFIELD_SERVER=http://localhost:8080 AGENTFIELD_API_KEY= +# AForge exec is the default. Set HARNESS_PROVIDER=opencode to roll back. +HARNESS_PROVIDER=aforge +AGENTFIELD_AFORGE_COMMAND=exec +# CLOUDSECURITY_AFORGE_BIN=/absolute/path/to/aforge + # Optional: Model overrides HARNESS_MODEL=openrouter/moonshotai/kimi-k2.5 AI_MODEL=openrouter/moonshotai/kimi-k2.5 diff --git a/Dockerfile b/Dockerfile index af1e36d..2decfb1 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>=0.1.0" \ + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python" \ "pydantic>=2.0" \ "httpx>=0.27" \ "python-dotenv>=1.0" \ @@ -27,7 +31,8 @@ FROM python:3.11-slim AS runtime ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ AGENTFIELD_SERVER=http://agentfield:8080 \ - HARNESS_PROVIDER=opencode \ + HARNESS_PROVIDER=aforge \ + AGENTFIELD_AFORGE_COMMAND=exec \ HARNESS_MODEL=openrouter/moonshotai/kimi-k2.5 \ AI_MODEL=openrouter/moonshotai/kimi-k2.5 \ PORT=8005 \ @@ -55,6 +60,7 @@ RUN mkdir -p /home/cloudsecurity/.config/opencode && \ chown -R cloudsecurity:cloudsecurity /home/cloudsecurity/.config COPY --from=builder /install /usr/local +COPY --from=aforge /aforge /usr/local/bin/aforge COPY src/ /app/src/ USER cloudsecurity diff --git a/README.md b/README.md index e1cc5c1..43a1206 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,9 @@ See [`docs/GITHUB_ACTIONS.md`](docs/GITHUB_ACTIONS.md) for full Tier 1 and Tier | `AGENTFIELD_SERVER` | No | `http://localhost:8080` | AgentField control plane URL | | `NODE_ID` | No | `cloudsecurity` | Agent node identifier | | `OPENROUTER_API_KEY` | Yes | - | Model provider credential | -| `CLOUDSECURITY_PROVIDER` | No | `opencode` | Harness provider override | +| `CLOUDSECURITY_PROVIDER` | No | `aforge` | Harness provider override; use `opencode` for rollback | +| `AGENTFIELD_AFORGE_COMMAND` | No | `exec` | AForge headless command (`do` remains an explicit override) | +| `CLOUDSECURITY_AFORGE_BIN` | No | `aforge` | Path to the AForge executable | | `CLOUDSECURITY_MODEL` | No | `openrouter/minimax/minimax-m2.5` | Harness model | | `CLOUDSECURITY_AI_MODEL` | No | `CLOUDSECURITY_MODEL`/`AI_MODEL` fallback | `.ai()` gate model | | `CLOUDSECURITY_MAX_TURNS` | No | `50` | Max turns per harness call | diff --git a/agentfield-package.yaml b/agentfield-package.yaml index 004149c..1b20aab 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -26,6 +26,14 @@ user_environment: description: Control-plane API key (if auth is enabled) type: secret scope: global + - name: HARNESS_PROVIDER + description: Coding-agent harness provider (aforge by default; opencode for rollback) + default: aforge + - name: AGENTFIELD_AFORGE_COMMAND + description: AForge headless command + default: exec + - name: CLOUDSECURITY_AFORGE_BIN + 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 diff --git a/docker-compose.yml b/docker-compose.yml index b73c0fe..4746445 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,8 @@ services: - AGENTFIELD_SERVER=http://agentfield:8080 - AGENTFIELD_API_KEY=${AGENTFIELD_API_KEY:-} - AGENT_CALLBACK_URL=http://cloudsecurity-af:8005 - - 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 d123652..ea9016e 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@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python", "pydantic>=2.0", "httpx>=0.27", "pyhcl2>=2.0", @@ -29,6 +29,9 @@ dev = [ [tool.hatch.build.targets.wheel] packages = ["src/cloudsecurity_af"] +[tool.hatch.metadata] +allow-direct-references = true + [tool.ruff] target-version = "py311" line-length = 120 diff --git a/src/cloudsecurity_af/app.py b/src/cloudsecurity_af/app.py index 94badff..3b0515c 100644 --- a/src/cloudsecurity_af/app.py +++ b/src/cloudsecurity_af/app.py @@ -44,6 +44,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/cloudsecurity_af/config.py b/src/cloudsecurity_af/config.py index 9a3ee21..0e7dc97 100644 --- a/src/cloudsecurity_af/config.py +++ b/src/cloudsecurity_af/config.py @@ -86,7 +86,7 @@ def from_input(cls, scan_input: CloudSecurityInput, repo_path: str) -> ScanConfi class AIIntegrationConfig(BaseModel): provider: str = Field( - default_factory=lambda: os.getenv("CLOUDSECURITY_PROVIDER", os.getenv("HARNESS_PROVIDER", "opencode")) + default_factory=lambda: os.getenv("CLOUDSECURITY_PROVIDER", os.getenv("HARNESS_PROVIDER", "aforge")) ) harness_model: str = Field( default_factory=lambda: os.getenv( @@ -102,6 +102,12 @@ class AIIntegrationConfig(BaseModel): ) max_turns: int = Field(default_factory=lambda: int(os.getenv("CLOUDSECURITY_MAX_TURNS", "50"))) opencode_bin: str = Field(default_factory=lambda: os.getenv("CLOUDSECURITY_OPENCODE_BIN", "opencode")) + aforge_bin: str = Field( + default_factory=lambda: os.getenv( + "CLOUDSECURITY_AFORGE_BIN", + os.getenv("AFORGE_BIN", "aforge"), + ) + ) @classmethod def from_env(cls) -> AIIntegrationConfig: @@ -125,6 +131,7 @@ def provider_env(self) -> dict[str, str]: "AZURE_SUBSCRIPTION_ID", ) 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 64a7cd1..900e389 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,16 +3,40 @@ import pytest from cloudsecurity_af.config import ( - BudgetConfig, - DepthProfile, DEPTH_CHAIN_LIMITS, DEPTH_HUNTER_MAP, DEPTH_PROVER_CAPS, + AIIntegrationConfig, + BudgetConfig, + DepthProfile, ScanConfig, ) from cloudsecurity_af.schemas.input import CloudSecurityInput +def test_aforge_exec_is_the_default_harness(monkeypatch: pytest.MonkeyPatch) -> None: + for key in ( + "CLOUDSECURITY_PROVIDER", + "HARNESS_PROVIDER", + "CLOUDSECURITY_AFORGE_BIN", + "AFORGE_BIN", + "AGENTFIELD_AFORGE_COMMAND", + ): + monkeypatch.delenv(key, raising=False) + + config = AIIntegrationConfig.from_env() + + assert config.provider == "aforge" + assert config.aforge_bin == "aforge" + assert config.provider_env()["AGENTFIELD_AFORGE_COMMAND"] == "exec" + + +def test_opencode_remains_an_explicit_rollback(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("HARNESS_PROVIDER", "opencode") + + assert AIIntegrationConfig.from_env().provider == "opencode" + + class TestDepthProfile: def test_enum_values(self) -> None: assert DepthProfile.QUICK.value == "quick"