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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
53 changes: 53 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Go

on:
push:
branches: ["main"]
paths:
- "go/**"
- ".github/workflows/go.yml"
pull_request:
branches: ["main"]
paths:
- "go/**"
- ".github/workflows/go.yml"

jobs:
go:
name: build / vet / test / gofmt
runs-on: ubuntu-latest
defaults:
run:
working-directory: go
steps:
- uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
# Track the module's own directive so a go.mod bump moves CI with it.
go-version-file: go/go.mod
cache-dependency-path: go/go.sum

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...

- name: Test
run: go test ./...

- name: Gofmt
run: test -z "$(gofmt -l .)"

docker:
name: docker build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# The Dockerfile COPYs go/go.mod and go/, so the build context is the
# REPO ROOT, not go/.
- name: Build the Go node image
run: docker build -f go/Dockerfile -t sec-af-go:test .
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,35 @@ af install https://github.com/Agent-Field/sec-af
af run sec-af
```

`af install` clones the repo, provisions an isolated Python environment, and registers the `sec-af` node with your control plane. On first `af run` you're prompted for the required `OPENROUTER_API_KEY` — stored encrypted and reused across every node, so you enter it only once. Then run an audit:
`af install` follows the repository manifest to the maintained Go package and registers it as the `sec-af` node with your control plane. If an older Python `sec-af` is installed, it is replaced in place, retaining the same node id, triggers, and node-scoped secrets. On first `af run` you're prompted for the required `OPENROUTER_API_KEY` — stored encrypted and reused across every node, so you enter it only once. Then run an audit:

```bash
af call sec-af.audit --in '{"repo_url": "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application"}'
```

New to AgentField? Install the control plane first with `curl -fsSL https://agentfield.ai/install.sh | bash`, or use the Docker / Railway options below.

To install the Python node deliberately, clone this repository and install the
checkout as a local path. Local-path installs do not follow `superseded_by`:

```bash
git clone https://github.com/Agent-Field/sec-af
af install ./sec-af
```

### Go implementation

The maintained node lives under [`go/`](go/README.md), and installing the bare
repository URL gives you this implementation as `sec-af` on its default port
`8013` — no Python environment is provisioned on that path. It registers the
same reasoners under the same names and draws the same control-plane DAG. The
Python implementation remains available through `python -m sec_af.app`, the root
Docker Compose stack, or the local-path install escape hatch (`git clone` then
`af install ./sec-af`). The Go add-on Compose file
(`docker-compose.go.yml`) uses the node id `sec-af-go` only so both
implementations can run against one control plane during a changeover. Build,
run, and Docker/compose docs live in [`go/README.md`](go/README.md).

### One-Click Deploy (Railway)

[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/sec-af)
Expand Down
15 changes: 15 additions & 0 deletions agentfield-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ version: 0.1.0
description: Security review agent node (scans repositories for vulnerabilities)
author: Agent-Field

# The Go node in go/ is the maintained security-audit node: same reasoners, same
# interface, one static binary, no per-node venv to build. Installing this repo
# installs that instead — so `af install https://github.com/Agent-Field/sec-af`
# is the one thing a user has to know, before and after the switch.
#
# go/ declares this same name deliberately, so the switch is a replacement in
# place: same node id, same triggers, node-scoped secrets kept. Only one of the
# two can be installed at a time, which is the point.
#
# This manifest stays here as the redirect, so the Python node is still what
# `python -m sec_af.app` and docker-compose run. The redirect is a git-install
# behaviour only: to install this node deliberately, clone the repo and install
# the checkout as a local path.
superseded_by: https://github.com/Agent-Field/sec-af//go

entrypoint:
start: python -m sec_af.app
healthcheck: /health
Expand Down
105 changes: 105 additions & 0 deletions docker-compose.go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# SEC-AF Go node — opt-in ADD-ON to the Python stack.
#
# The Python docker-compose.yml is the DEFAULT stack (the AgentField control
# plane `agentfield` + the Python `sec-af` node on :8003) and is left 100%
# untouched. This file adds ONLY the Go node, registered under a DISTINCT
# identity so both nodes can run against one control plane simultaneously:
#
# sec-af-go -> node id "sec-af-go", :8013
#
# Run story (two commands, Python stack first):
#
# docker compose up -d # Python stack + control plane
# docker compose -f docker-compose.go.yml up -d # adds the Go node
#
# This is a SEPARATE compose project (name: sec-af-go) that joins the Python
# stack's network as an EXTERNAL reference, so AGENTFIELD_SERVER=
# http://agentfield:8080 resolves and the Go node shares the Python stack's
# workspaces volume over that network. The control plane (service `agentfield`)
# lives in the Python project, so there is NO `depends_on` here — bring the
# Python stack up first.
#
# AND WAIT FOR IT TO ANSWER. Unlike the Python node, which keeps serving in
# "degraded mode" and retries registration every 10s, the Go SDK returns the
# registration error from Serve and cmd/sec-af treats it as fatal — so this
# container EXITS if the control plane is not reachable at start, and
# `restart: unless-stopped` below turns that into a restart loop until it is.
# The same applies when the control plane is restarted under a running Go node.
# See "Parity notes" in go/README.md.
#
# COMPOSE_PROJECT_NAME caveat: the external network/volume names below
# (sec-af_default, sec-af_workspaces) are the Python project's
# default-project-name resources. The Python docker-compose.yml has NO explicit
# `name:`, so its project name defaults to the compose directory's basename —
# `sec-af` when the repo is checked out as a directory named `sec-af`. If you
# set COMPOSE_PROJECT_NAME for the Python stack (or the checkout directory is
# named something else), override the external `name:` fields below to match
# `<project>_default` and `<project>_workspaces`.
name: sec-af-go

services:
sec-af-go:
build:
context: .
dockerfile: go/Dockerfile
args:
AFORGE_BASE_URL: ${AFORGE_BASE_URL:-https://agentfield.ai/downloads/aforge}
AFORGE_VERSION: ${AFORGE_VERSION:-v0.1.0}
environment:
- AGENTFIELD_SERVER=http://agentfield:8080 # CP service name in sec-af's compose is "agentfield"
- AGENTFIELD_API_KEY=${AGENTFIELD_API_KEY:-}
- NODE_ID=sec-af-go
- PORT=8013
- AGENT_CALLBACK_URL=http://sec-af-go:8013
- 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}
# SEC_AF_AFORGE_BIN is deliberately NOT forwarded, exactly as the Python
# sec-af service does not forward it (docker-compose.yml). Compose loads
# the repo-root .env for BOTH stacks and .env.example documents
# `SEC_AF_AFORGE_BIN=/absolute/path/to/aforge` as the way to point a
# non-container run at a HOST binary; a bare `- SEC_AF_AFORGE_BIN`
# passthrough would inject that host path into the container, where
# config.AIConfigFromEnv resolves it as the harness BinPath and every
# harness call fails on a missing executable instead of using the image's
# checksum-verified /usr/local/bin/aforge (go/Dockerfile).
# XDG_DATA_HOME is deliberately NOT set, matching the Python sec-af
# service (docker-compose.yml), so config.ProviderEnv falls back to
# <tempdir>/opencode-shared-data in both containers — same directory,
# same (ephemeral) lifetime. See go/Dockerfile for the long form.
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
- SEC_AF_WORKSPACES_DIR=/workspaces
ports:
- "8013:8013"
volumes:
- workspaces:/workspaces
# Same cadence as the Python sec-af service (docker-compose.yml) and as the
# image's own HEALTHCHECK (go/Dockerfile), so both nodes in one stack report
# health on the same schedule. A compose-level healthcheck SUPERSEDES the
# image directive, so these values are the ones that actually run.
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8013/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped

# Join the Python stack's default network so `agentfield` (the control plane)
# resolves by service name. external => Compose does NOT create it; the Python
# stack must be up first (see COMPOSE_PROJECT_NAME caveat in the header).
networks:
default:
external: true
name: sec-af_default

# Share the Python stack's workspaces volume (<project>_<volume> =
# sec-af_workspaces). There is no opencode-data volume: the Python sec-af
# service does not declare one either, and without XDG_DATA_HOME the opencode
# shared-data directory lives under the container's tempdir, exactly as it does
# for the Python node.
volumes:
workspaces:
external: true
name: sec-af_workspaces
9 changes: 9 additions & 0 deletions go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
bin/
go.work
go.work.sum
coverage.out
*.test

# Python bytecode from scripts/gen_*.py (the generators run under the repo's venv).
__pycache__/
*.pyc
138 changes: 138 additions & 0 deletions go/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# SEC-AF Go node — multi-stage build.
#
# Build from the SEC-AF repo ROOT so the go/ module is in the build context and
# the paths below (go/go.mod, go/) resolve. docker-compose.go.yml builds it
# exactly this way (build.context: ., dockerfile: go/Dockerfile):
#
# docker build -f go/Dockerfile -t sec-af-go:latest .
#
# The AgentField Go SDK is a REAL versioned require resolved from
# proxy.golang.org, so there is no SDK clone stage, no GOWORK=off and no
# `replace` dance: `go mod download` pulls everything from the module proxy,
# cache-keyed on go.mod/go.sum.

# ---------------------------------------------------------------------------
# Stage 0 — aforge: fetch the released AForge CLI from the public download host
# and verify it against the release checksums (which hash the DECOMPRESSED
# binaries). Lifted from this repo's own Python Dockerfile so both images ship
# the identical, checksum-verified binary. Both ARGs are overridable:
#
# docker build --build-arg AFORGE_BASE_URL=... --build-arg AFORGE_VERSION=... .
# ---------------------------------------------------------------------------
FROM debian:bookworm-slim AS aforge

ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge
ARG AFORGE_VERSION=v0.1.0
# Provided automatically by BuildKit; defaults to the builder's own arch.
ARG TARGETARCH

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

WORKDIR /out

RUN set -eux; \
arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${arch}.gz" -o aforge.gz; \
gunzip -c aforge.gz > aforge; \
rm -f 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 -f checksums.txt aforge.sha256; \
chmod +x aforge


# ---------------------------------------------------------------------------
# Stage 1 — builder: fetch modules from the proxy, build the static binary.
# Go 1.23 satisfies go.mod's `go 1.21` directive.
# ---------------------------------------------------------------------------
FROM golang:1.23-bookworm AS builder

WORKDIR /src

# Prime the module cache from go.mod/go.sum first so dependency downloads cache
# independently of source edits (this layer re-runs only when they change).
COPY go/go.mod go/go.sum ./
RUN go mod download

COPY go/ ./
ENV CGO_ENABLED=0 GOOS=linux
RUN go build -trimpath -ldflags="-s -w" -o /out/sec-af ./cmd/sec-af


# ---------------------------------------------------------------------------
# Stage 2 — runtime: slim Debian mirroring the Python image (opencode CLI + a
# non-root secaf user), shipping the single static Go binary instead of a Python
# runtime. The entrypoint generates opencode.json from HARNESS_MODEL at
# container start, so the env var is honored instead of a baked-in model.
# ---------------------------------------------------------------------------
FROM debian:bookworm-slim AS runtime

ARG OPENCODE_VERSION=1.17.15

# AGENTFIELD_SERVER is deliberately NOT baked in: sec-af's Python image does not
# set it either (Dockerfile runtime ENV block), so both images fall back to the
# node's own default, http://localhost:8080 (app.py:39, go/internal/node/node.go:180)
# — which is what go/README.md's env table documents. docker-compose.go.yml sets
# it explicitly for the compose stack; a bare `docker run` keeps the localhost
# default instead of reaching for a hostname that only exists inside compose.
ENV HARNESS_PROVIDER=aforge \
AGENTFIELD_AFORGE_COMMAND=exec \
HARNESS_MODEL=openrouter/minimax/minimax-m2.5 \
AI_MODEL=openrouter/minimax/minimax-m2.5 \
PORT=8013 \
NODE_ID=sec-af \
HOME=/home/secaf \
PATH=/home/secaf/.opencode/bin:${PATH} \
SEC_AF_WORKSPACES_DIR=/workspaces

# XDG_DATA_HOME is deliberately NOT set, for the same reason as
# AGENTFIELD_SERVER above: sec-af's Python image does not set it (Dockerfile
# runtime ENV block) and neither does its compose file, so
# `AIIntegrationConfig.provider_env()` (src/sec_af/config.py:123) takes its
# `os.path.join(tempfile.gettempdir(), "opencode-shared-data")` fallback in the
# container. Setting it here — as the pr-af image does, where BOTH sides set it
# and it IS parity — would give the Go node a different opencode data directory
# with a different lifetime (a persistent volume instead of an ephemeral /tmp)
# and would mean config.ProviderEnv never exercises the fallback branch it was
# written to port.

# System deps: ca-certificates (HTTPS to the LLM provider + git hosts), curl
# (healthcheck + opencode installer), git (audit clones the target repository).
# Create the non-root secaf user (uid/gid 10001) and install the opencode CLI as
# that user so it lands under /home/secaf/.opencode (on PATH above).
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git && \
groupadd --gid 10001 secaf && \
useradd --uid 10001 --gid secaf --create-home --home-dir /home/secaf --shell /bin/sh secaf && \
su -s /bin/sh secaf -c "curl -fsSL https://opencode.ai/install | bash -s -- --version ${OPENCODE_VERSION} --no-modify-path" && \
mkdir -p /workspaces && \
chown -R secaf:secaf /workspaces /home/secaf && \
rm -rf /var/lib/apt/lists/*

COPY --from=builder /out/sec-af /usr/local/bin/sec-af
COPY --from=aforge /out/aforge /usr/local/bin/aforge
COPY go/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

USER secaf
# Cwd must be writable by secaf: the AgentField Go SDK creates its schema output
# dir under the process cwd when a harness call carries no Cwd, and `/` is
# root-owned. /workspaces is secaf-owned and is SEC_AF_WORKSPACES_DIR already.
WORKDIR /workspaces

EXPOSE 8013

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8013/health || exit 1

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["/usr/local/bin/sec-af"]
Loading
Loading