Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ 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: build-time overrides for where the AForge CLI is downloaded from
# (consumed by `docker compose build`, not by the running agent).
# AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge
# AFORGE_VERSION=v0.1.0

# Optional: Model overrides
HARNESS_MODEL=openrouter/moonshotai/kimi-k2.5
AI_MODEL=openrouter/moonshotai/kimi-k2.5
Expand Down
40 changes: 38 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
# The AForge CLI is fetched from the public release download host rather than
# pulled from a container registry, so the build needs no registry credentials.
# Both ARGs are overridable (--build-arg) to point at a mirror or a newer build.
ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge
ARG AFORGE_VERSION=v0.1.0

FROM debian:bookworm-slim AS aforge

ARG AFORGE_BASE_URL
ARG AFORGE_VERSION
ARG TARGETARCH

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl && \
rm -rf /var/lib/apt/lists/*

# Download aforge-linux-<arch>.gz, decompress it, and verify the *decompressed*
# binary against the release checksums file (which hashes the raw binaries).
RUN set -eux; \
arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
mkdir -p /out; \
cd /out; \
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; \
grep " aforge-linux-${arch}$" checksums.txt | 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

ENV PYTHONDONTWRITEBYTECODE=1 \
Expand All @@ -14,7 +48,7 @@ COPY pyproject.toml README.md ./
COPY src/ src/

RUN pip install --no-cache-dir --prefix=/install \
"agentfield>=0.1.0" \
"agentfield>=0.1.130" \
"pydantic>=2.0" \
"httpx>=0.27" \
"python-dotenv>=1.0" \
Expand All @@ -27,7 +61,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 \
Expand Down Expand Up @@ -55,6 +90,7 @@ RUN mkdir -p /home/cloudsecurity/.config/opencode && \
chown -R cloudsecurity:cloudsecurity /home/cloudsecurity/.config

COPY --from=builder /install /usr/local
COPY --from=aforge /out/aforge /usr/local/bin/aforge
COPY src/ /app/src/

USER cloudsecurity
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,13 +326,33 @@ 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 the SDK runs: `exec` (default) or `do`. Read by agentfield>=0.1.130 |
| `CLOUDSECURITY_AFORGE_BIN` | No | `aforge` | Path to the AForge executable (falls back to `AFORGE_BIN`) |
| `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 |
| `CLOUDSECURITY_REPO_PATH` | No | cwd | Local repository path fallback |
| `AGENT_CALLBACK_URL` | No | `http://127.0.0.1:8004` | Agent callback endpoint |

### AForge CLI in the Docker image

The image downloads the released AForge CLI at build time, decompresses it, and
verifies its SHA-256 against the release `checksums.txt` before installing it to
`/usr/local/bin/aforge`. Two build args control where it comes from:

| Build arg | Default | Purpose |
|---|---|---|
| `AFORGE_BASE_URL` | `https://agentfield.ai/downloads/aforge` | Download host serving `<version>/aforge-linux-<arch>.gz` and `<version>/checksums.txt` |
| `AFORGE_VERSION` | `v0.1.0` | Released AForge version to install |

```bash
docker build --build-arg AFORGE_VERSION=v0.1.0 -t cloudsecurity-af .
```

`docker compose build` reads the same two values from the environment (or `.env`),
so a mirror can be selected without editing the Dockerfile.

### Core `CloudSecurityInput` Fields

- `repo_url`, `branch`, `commit_sha`, `base_commit_sha`
Expand Down
8 changes: 8 additions & 0 deletions agentfield-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
AFORGE_BASE_URL: ${AFORGE_BASE_URL:-https://agentfield.ai/downloads/aforge}
AFORGE_VERSION: ${AFORGE_VERSION:-v0.1.0}
ports:
- "8005:8005"
environment:
- 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}
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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>=0.1.130",
"pydantic>=2.0",
"httpx>=0.27",
"pyhcl2>=2.0",
Expand Down
1 change: 1 addition & 0 deletions src/cloudsecurity_af/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
9 changes: 8 additions & 1 deletion src/cloudsecurity_af/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:
Expand All @@ -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
Expand Down
61 changes: 59 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,73 @@
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"


def test_aforge_bin_is_overridable(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("CLOUDSECURITY_AFORGE_BIN", raising=False)
monkeypatch.setenv("AFORGE_BIN", "/opt/aforge/bin/aforge")

assert AIIntegrationConfig.from_env().aforge_bin == "/opt/aforge/bin/aforge"

monkeypatch.setenv("CLOUDSECURITY_AFORGE_BIN", "/usr/local/bin/aforge")

assert AIIntegrationConfig.from_env().aforge_bin == "/usr/local/bin/aforge"


def test_installed_sdk_supports_the_aforge_harness(monkeypatch: pytest.MonkeyPatch) -> None:
"""The pinned agentfield floor must accept the provider/bin this agent wires up."""
from agentfield import HarnessConfig

for key in ("CLOUDSECURITY_PROVIDER", "HARNESS_PROVIDER", "CLOUDSECURITY_AFORGE_BIN", "AFORGE_BIN"):
monkeypatch.delenv(key, raising=False)
config = AIIntegrationConfig.from_env()

harness = 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.provider == "aforge"
assert harness.aforge_bin == "aforge"


class TestDepthProfile:
def test_enum_values(self) -> None:
assert DepthProfile.QUICK.value == "quick"
Expand Down