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
78 changes: 78 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# CI for the Go node under go/. The Python tree has no workflow in this repo, so
# this file is scoped to the Go port only: it runs the exact gate the port
# contract defines (go/docs/DESIGN.md §0.4) plus a Docker build of the image the
# compose add-on ships.
#
# The paths filter is NOT just go/**: go/packaging_test.go asserts the Go
# packaging against the Python stack's own files, so those are inputs to this
# gate as well. go/packaging_test.go's own
# TestPackaging_CIWatchesEveryFileTheGateReads keeps the two lists in sync.
name: Go

on:
push:
branches: ["main"]
paths:
- "go/**"
- "docker-compose.go.yml"
- ".github/workflows/go.yml"
# go/packaging_test.go derives its expectations from the PYTHON stack, so
# every file it reads has to trigger this workflow — otherwise a change to
# one of them merges with the parity gate never running, and the breakage
# surfaces later as a `premise broken` failure on an unrelated go/** PR.
# go/Dockerfile's aforge stage is a verbatim copy of the root one, so the
# docker-build job needs them too.
- "Dockerfile"
- "docker-compose.yml"
- "README.md"
- "agentfield-package.yaml"
- "src/cloudsecurity_af/app.py"
pull_request:
branches: ["main"]
paths:
- "go/**"
- "docker-compose.go.yml"
- ".github/workflows/go.yml"
# go/packaging_test.go derives its expectations from the PYTHON stack, so
# every file it reads has to trigger this workflow — otherwise a change to
# one of them merges with the parity gate never running, and the breakage
# surfaces later as a `premise broken` failure on an unrelated go/** PR.
# go/Dockerfile's aforge stage is a verbatim copy of the root one, so the
# docker-build job needs them too.
- "Dockerfile"
- "docker-compose.yml"
- "README.md"
- "agentfield-package.yaml"
- "src/cloudsecurity_af/app.py"

jobs:
go:
runs-on: ubuntu-latest
defaults:
run:
working-directory: go
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
# Track go/go.mod's own directive so a bump lands in one place.
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-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 — exactly how docker-compose.go.yml builds it.
- name: Build Docker image
run: docker build -f go/Dockerfile -t cloudsecurity-af-go:test .
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,25 @@ Already running an [AgentField](https://github.com/Agent-Field/agentfield) contr

```bash
af install https://github.com/Agent-Field/cloudsecurity-af
af run cloudsecurity
af run cloudsecurity-af
```

`af install` clones the repo, provisions an isolated Python environment, and registers the `cloudsecurity` 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 scan some IaC:
`af install` follows the repository manifest to the maintained Go package and registers it as the `cloudsecurity` node with your control plane — one static binary, no per-node virtualenv to build. (`af run` takes the PACKAGE name, `cloudsecurity-af`; `af call` takes the node id, `cloudsecurity`.) If an older Python `cloudsecurity` 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 scan some IaC:

```bash
af call cloudsecurity.scan --in '{"repo_url": "https://github.com/org/infra-repo"}'
```

New to AgentField? Install the control plane first with `curl -fsSL https://agentfield.ai/install.sh | bash`, or use the Docker option 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/cloudsecurity-af
af install ./cloudsecurity-af
```

### Local (Docker Compose)

```bash
Expand Down Expand Up @@ -386,6 +394,30 @@ Package metadata:
- License: Apache-2.0
- Core deps: `agentfield`, `pydantic>=2.0`, `pyhcl2>=2.0`

## Go implementation

The maintained node lives under [`go/`](go/README.md), and installing the bare
repository URL gives you this implementation as the `cloudsecurity` node.
(`af run` assigns a free port from 8001 and exports it as `PORT`; `8015` is the
binary's own default, which is what a bare `go run`, `make run` or
`docker-compose.go.yml` gives you — pass `af run cloudsecurity-af --port 8015`
to pin it.) It registers the same reasoners under the same names,
drives the same control-plane DAG, and reads the same environment variables —
one static binary, no per-node virtualenv to build. The Python implementation
remains available through `python -m cloudsecurity_af.app`, the root Docker
Compose stack, or a local-path install (`af install ./cloudsecurity-af`), which
does not follow the redirect.

```bash
docker compose up -d # Python stack (control plane + cloudsecurity-af :8005)
docker compose -f docker-compose.go.yml up -d # adds the Go node as cloudsecurity-go :8015
```

The Go add-on Compose file uses the distinct node id `cloudsecurity-go` only so
both implementations can run against one control plane during a changeover.
Build, run, Docker/compose and environment docs live in
[`go/README.md`](go/README.md).

## Open Core Model

CloudSecurity uses an open-core model: `scan` and `prove` remain open source (Apache 2.0), while enterprise adds org-scale controls such as multi-account management, scheduled monitoring, and RBAC/audit features. See [`docs/OPEN_CORE.md`](docs/OPEN_CORE.md) for the full tier breakdown.
Expand Down
25 changes: 24 additions & 1 deletion agentfield-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,35 @@ version: 0.1.0
description: Cloud security posture agent node (read-only scans of AWS/GCP/Azure)
author: Agent-Field

# The Go node in go/ is the maintained CloudSecurity 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/cloudsecurity-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 cloudsecurity_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/cloudsecurity-af//go

entrypoint:
start: python -m cloudsecurity_af.app
healthcheck: /health

agent_node:
node_id: cloudsecurity-af
# `cloudsecurity`, not `cloudsecurity-af`: src/cloudsecurity_af/app.py uses
# `NODE_ID = os.getenv("NODE_ID", "cloudsecurity")`, so that is the id the
# process registers and reports on /health, and every reasoner target in
# reasoners/phases.py and orchestrator.py is f"{NODE_ID}.<reasoner>".
# `af run` compares the manifest's node_id against /health's node_id and
# kills the process when they differ, so the stale `cloudsecurity-af` broke
# the local-path install documented above.
node_id: cloudsecurity
default_port: 8005

user_environment:
Expand Down
114 changes: 114 additions & 0 deletions docker-compose.go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# CloudSecurity-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 `cloudsecurity-af` node on :8005) 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:
#
# cloudsecurity-go -> node id "cloudsecurity-go", :8015
#
# 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: cloudsecurity-af-go) that joins the
# Python stack's network as an EXTERNAL reference, so
# AGENTFIELD_SERVER=http://agentfield:8080 resolves. The control plane (service
# `agentfield`) lives in the Python project, so there is NO `depends_on` here —
# bring the Python stack up first.
#
# COMPOSE_PROJECT_NAME caveat: the external network name below
# (cloudsecurity-af_default) is the Python project's default-project-name
# resource. The Python docker-compose.yml has NO explicit `name:`, so its
# project name defaults to the compose directory's basename —
# `cloudsecurity-af` when the repo is checked out as a directory of that name.
# If you set COMPOSE_PROJECT_NAME for the Python stack (or the checkout
# directory is named something else), override the external `name:` below to
# match `<project>_default`.
name: cloudsecurity-af-go

services:
cloudsecurity-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 cloudsecurity-af's compose is "agentfield"
- AGENTFIELD_API_KEY=${AGENTFIELD_API_KEY:-}
- NODE_ID=cloudsecurity-go
- PORT=8015
- AGENT_CALLBACK_URL=http://cloudsecurity-go:8015
- 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}
# XDG_DATA_HOME is set here and NOT in docker-compose.yml — a deliberate
# packaging difference (divergence 6 in go/README.md). Both nodes read the
# variable identically and fall back to <tmpdir>/opencode-shared-data;
# pointing it at the opencode-data volume below keeps the harness's data
# home across container restarts, which the Python stack does not do.
- XDG_DATA_HOME=/home/cloudsecurity/.local/share
# SEC_AF_WORKSPACES_DIR is deliberately unset, exactly as in
# docker-compose.yml: the /workspaces bind below is owned by the HOST uid,
# not by the image's cloudsecurity user, so the node must be allowed to
# run app.py::_workspaces_root's write probe and fall back to
# ~/.sec-af/workspaces. Setting it skips the probe and turns every remote
# repo_url clone into an HTTP 500.
# Cloud provider credentials (read-only, for prove mode)
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-us-east-1}
- GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS:-}
- AZURE_SUBSCRIPTION_ID=${AZURE_SUBSCRIPTION_ID:-}
- AZURE_TENANT_ID=${AZURE_TENANT_ID:-}
- AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}
- AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}
ports:
- "8015:8015"
volumes:
# The Python stack does NOT use a named workspaces volume — its compose
# bind-mounts ${SCAN_REPOS_PATH:-./workspaces} into /workspaces. Mirror
# that bind (same variable, same default) so pointing SCAN_REPOS_PATH at a
# host directory gives both nodes the same clones.
#
# It only does that when that directory is writable by uid 10001, the user
# both images run as. With the DEFAULT ./workspaces, Docker auto-creates
# the bind target root-owned, the write probe above fails, and each node
# falls back to its own container-local ~/.sec-af/workspaces — two
# separate checkouts, which is correct behaviour (it matches the Python
# node exactly) but is not a shared host directory.
- ${SCAN_REPOS_PATH:-./workspaces}:/workspaces
- opencode-data:/home/cloudsecurity/.local/share
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8015/health"]
# Same cadence as this repo's Python node (docker-compose.yml:53-58) and
# as the image's own HEALTHCHECK (go/Dockerfile), so the two nodes in one
# stack report unhealthy on the same schedule. The 60s/30s/5/30s values
# this file shipped with were pr-af's, where they matched pr-af's Python
# compose; here they made the Go node take ~5 minutes to go unhealthy
# against the Python node's 90 seconds.
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: cloudsecurity-af_default

volumes:
# Node-local opencode data (XDG_DATA_HOME above). The Python compose has no
# counterpart: it leaves XDG_DATA_HOME unset, so its harness data home is a
# container-local tmpdir. The workspaces path, by contrast, is a host bind and
# deliberately not a volume — see the service's SEC_AF_WORKSPACES_DIR note.
opencode-data: {}
5 changes: 5 additions & 0 deletions go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bin/
go.work
go.work.sum
coverage.out
*.test
Loading
Loading