diff --git a/control-plane/Dockerfile b/control-plane/Dockerfile index c36bb4e..b7f0d6c 100644 --- a/control-plane/Dockerfile +++ b/control-plane/Dockerfile @@ -63,6 +63,7 @@ COPY policies/default.yaml /etc/agentlock/policies/default.yaml ENV AGENTLOCK_LISTEN=0.0.0.0:7878 ENV AGENTLOCK_DASHBOARD_LISTEN=0.0.0.0:7879 ENV AGENTLOCK_HOME=/var/lib/agentlock +ENV AGENTLOCK_POLICY=/etc/agentlock/policies/default.yaml ENV AGENTLOCK_IN_CONTAINER=1 # Default to unattested so `docker run` works zero-conf. TUI + dashboard # show the red "UNATTESTED — LEDGER NOT SIGNED" banner. Override with diff --git a/control-plane/cmd/control-plane/main.go b/control-plane/cmd/control-plane/main.go index 095fbfa..84f556e 100644 --- a/control-plane/cmd/control-plane/main.go +++ b/control-plane/cmd/control-plane/main.go @@ -31,6 +31,10 @@ func main() { if addr == "" { addr = "127.0.0.1:7878" } + if len(os.Args) > 1 && os.Args[1] == "--health" { + runHealthProbe(addr) + return + } home := os.Getenv("AGENTLOCK_HOME") if home == "" { log.Fatalf("AGENTLOCK_HOME is required (ledger state lives there)") @@ -116,6 +120,42 @@ func main() { log.Printf("control-plane stopped") } +// runHealthProbe is invoked when the binary is run as `agentlockd --health` +// (e.g. from the docker HEALTHCHECK). Distroless has no curl/wget, so we +// reuse the binary itself as the probe. The listen addr may be 0.0.0.0:port; +// rewrite to 127.0.0.1 for the probe. +func runHealthProbe(listen string) { + probeAddr := listen + if h, p, ok := splitHostPort(listen); ok && (h == "" || h == "0.0.0.0" || h == "::") { + probeAddr = "127.0.0.1:" + p + } + client := &http.Client{Timeout: 1500 * time.Millisecond} + resp, err := client.Get("http://" + probeAddr + "/v1/health") + if err != nil { + log.Printf("healthcheck: %v", err) + os.Exit(1) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + log.Printf("healthcheck: status %d", resp.StatusCode) + os.Exit(1) + } +} + +func splitHostPort(addr string) (host, port string, ok bool) { + i := -1 + for j := len(addr) - 1; j >= 0; j-- { + if addr[j] == ':' { + i = j + break + } + } + if i < 0 { + return "", "", false + } + return addr[:i], addr[i+1:], true +} + // loadPolicy reads $AGENTLOCK_POLICY if set; otherwise returns a built-in // safe default (monitor mode, destructive-bash only) so the daemon always // starts with *some* policy bound to session attestations. diff --git a/docker-compose.yml b/docker-compose.yml index b177680..e373d89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,6 @@ services: environment: AGENTLOCK_LISTEN: "0.0.0.0:7878" AGENTLOCK_DASHBOARD_LISTEN: "0.0.0.0:7879" - AGENTLOCK_POLICY: "/etc/agentlock/policies/default.yaml" AGENTLOCK_HOME: "/var/lib/agentlock" volumes: - agentlock-state:/var/lib/agentlock diff --git a/policies/default.yaml b/policies/default.yaml index ac039bd..3faa52d 100644 --- a/policies/default.yaml +++ b/policies/default.yaml @@ -23,10 +23,10 @@ gates: command_regex: '^(pip|pip3|npm|pnpm|yarn|brew|cargo|gem) (install|add|-i)\b' evaluate: - kind: typosquat - reference: policies/lists/pkg-allowlist.txt + reference: __INLINE__:numpy,pandas,requests,flask,django,fastapi,pytest,pip,setuptools,wheel,react,vue,express,lodash,axios,typescript,eslint,prettier,jest,vite,git,node,python,go,rust,docker,kubectl action_on_near_miss: deny - kind: allowlist - list: policies/lists/pkg-allowlist.txt + list: __INLINE__:numpy,pandas,requests,flask,django,fastapi,pytest,pip,setuptools,wheel,react,vue,express,lodash,axios,typescript,eslint,prettier,jest,vite,git,node,python,go,rust,docker,kubectl on_hit: allow on_miss: deny @@ -61,7 +61,7 @@ gates: - { tool: mcp.http.request } evaluate: - kind: host-allowlist - list: policies/lists/net-allowlist.txt + list: __INLINE__:github.com,raw.githubusercontent.com,api.github.com,registry.npmjs.org,pypi.org,files.pythonhosted.org,crates.io,static.crates.io,rubygems.org,pkg.go.dev,proxy.golang.org,sum.golang.org on_hit: allow on_miss: deny