From 63c56b11b1b6a7d8094468bc6a90a2f7858e0c84 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Mon, 8 Jun 2026 19:32:29 +0200 Subject: [PATCH] docs: audit and refresh docs to match shipped features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring README, user guide, AI reference, and `aigate help-ai` back in sync with the code after the dashboard, IPv6, masking, and Go-version changes. - Document `aigate serve` (local web dashboard) and the `~/.aigate/audit.jsonl` audit log (run_started / blocked events) — previously undocumented entirely. - Sync the default-config example in the user guide with InitDefaultConfig: add *.p12, terraform.tfstate, *.tfvars; ncat/netcat/rsync/ftp; registry.npmjs.org, proxy.golang.org; and the mask_stdout block init writes. - Add the aws_secret preset (6 presets, not 5) to the user guide table and help-ai; fix its description (matches AWS_SECRET_ACCESS_KEY= assignments). - README: Go badge 1.24+ -> 1.25+; drop the "cgroups limits enforced" claim (the user guide already notes limits are not yet enforced). - AI reference: add audit_service.go, masker.go, internal/web/, setup.go, help_ai.go, and the escape tests to the architecture map; note the append-only audit log + read-only dashboard design. - help-ai: add `doctor` to setup and a DASHBOARD & AUDIT LOG section. Verified: go build ./... and go test -short ./... pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 5 +++-- actions/help_ai.go | 25 ++++++++++++++++------- docs/AI/README.md | 21 +++++++++++++++---- docs/user/README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 69b72ed..d52fd8d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
AiGate

- Go Version + Go Version OS Support License

@@ -53,7 +53,7 @@ AI coding tools rely on application-level permission systems that can be bypasse - **Network isolation** - `bwrap --unshare-net` + `slirp4netns` + `iptables` (+ `ip6tables` for IPv6 when available) restrict egress to allowed domains (Linux) - **Command blocking** - Deny execution of dangerous commands (curl, wget, ssh) - **Output masking** - Redact secrets (API keys, tokens) from stdout/stderr before they reach the terminal -- **Resource limits** - cgroups v2 enforce memory, CPU, PID limits (Linux) +- **Audit log + dashboard** - Every run and blocked command is recorded to `~/.aigate/audit.jsonl`; `aigate serve` exposes a local web dashboard over it - **Tool-agnostic** - Works with any AI tool: Claude Code, Cursor, Copilot, Aider - **Sensible defaults** - Ships with deny rules for .env, secrets/, .ssh/, *.pem, etc. - **Project-level config** - `.aigate.yaml` extends global rules per project @@ -79,6 +79,7 @@ aigate deny net --except api.anthropic.com # Restrict network aigate allow read .env # Remove a deny rule aigate run -- claude # Run AI tool in sandbox aigate status # Show current rules +aigate serve # Local web dashboard over the audit log aigate help-ai # Show AI-friendly usage examples aigate reset --force # Remove everything ``` diff --git a/actions/help_ai.go b/actions/help_ai.go index e9b5a4b..a97b75a 100644 --- a/actions/help_ai.go +++ b/actions/help_ai.go @@ -24,6 +24,7 @@ SETUP (one-time) sudo aigate setup # Create OS group "ai-agents" and user "ai-runner" aigate init # Create default config at ~/.aigate/config.yaml aigate init --force # Re-create config (overwrites existing) + aigate doctor # Check prerequisites and active isolation mode FILE RESTRICTIONS (deny read) aigate deny read .env # Block a single file @@ -53,6 +54,14 @@ RUNNING SANDBOXED CHECKING STATUS aigate status # Show all rules, group/user, limits +DASHBOARD & AUDIT LOG + Every run and blocked command is appended to ~/.aigate/audit.jsonl (JSON Lines). + Events: run_started (rule counts) and blocked (matched deny_exec rule). + + tail -f ~/.aigate/audit.jsonl # Follow events as plain text + aigate serve # Local web dashboard → http://127.0.0.1:8080 + aigate serve --addr 127.0.0.1:9000 # Custom address (or AIGATE_ADDR env var) + CONFIGURATION Global config: ~/.aigate/config.yaml Project config: .aigate.yaml (in project root, merged with global) @@ -90,6 +99,7 @@ CONFIGURATION - openai - anthropic - aws_key + - aws_secret - github - bearer @@ -116,13 +126,14 @@ OUTPUT MASKING (mask_stdout) addition to kernel-level sandbox protections (defense-in-depth). Built-in presets: - openai sk-... / sk-proj-... → sk-*** - anthropic sk-ant-... → sk-ant-*** - aws_key AKIA... (access key ID) → AKIA*** - github ghp_, gho_, ghu_, ghs_, ghr_ → ghp_*** - bearer Bearer → Bearer *** + openai sk-... / sk-proj-... → sk-*** + anthropic sk-ant-... → sk-ant-*** + aws_key AKIA... (access key ID) → AKIA*** + aws_secret AWS_SECRET_ACCESS_KEY=... → AWS_SECRET_ACCESS_KEY=*** + github ghp_, gho_, ghu_, ghs_, ghr_ → ghp_*** + bearer Bearer → Bearer *** - All 5 presets are enabled by default (aigate init). + All 6 presets are enabled by default (aigate init). Pattern options: regex RE2-compatible regular expression (required) @@ -146,7 +157,7 @@ WHAT THE AI AGENT SEES INSIDE THE SANDBOX [aigate] deny_read: .env, secrets/, *.pem [aigate] deny_exec: curl, wget, ssh [aigate] allow_net: api.anthropic.com (all other outbound connections will be blocked) - [aigate] mask_stdout: openai, anthropic, aws_key, github, bearer + [aigate] mask_stdout: openai, anthropic, aws_key, aws_secret, github, bearer Denied files contain a marker instead of their content: [aigate] access denied: this file is protected by sandbox policy. See /tmp/.aigate-policy for all active restrictions. diff --git a/docs/AI/README.md b/docs/AI/README.md index 0f1a5aa..adf8930 100644 --- a/docs/AI/README.md +++ b/docs/AI/README.md @@ -15,28 +15,40 @@ domain/ Pure data structures services/ Core business logic platform.go Platform interface + Executor interface + resolvePatterns - platform_linux.go Linux: setfacl, groupadd/useradd, RunSandboxed dispatch + platform_linux.go Linux: setfacl, groupadd/useradd, RunSandboxed dispatch, splitDNSByFamily platform_linux_bwrap.go Linux bwrap path: buildBwrapArgs, runWithBwrap, runWithBwrapNetFilter platform_darwin.go macOS: chmod +a, dscl, sandbox-exec - config_service.go Config load/save/merge (global + project) + config_service.go Config load/save/merge (global + project), InitDefaultConfig rule_service.go Rule CRUD (add/remove/list deny rules) runner_service.go Sandboxed process launcher + masker.go MaskingWriter + builtin mask_stdout presets + audit_service.go Append/read ~/.aigate/audit.jsonl; AuditWriter wraps run output + +internal/web/ Local dashboard (aigate serve) + server.go HTTP server (loopback by default) + handlers.go JSON API + page handlers over the audit log + static/, templates/ Embedded JS/CSS/HTML assets actions/ CLI command handlers - init.go Create group, user, default config + setup.go Create OS group + user (sudo) + init.go Write default config deny.go Add deny rules (read, exec, net subcommands) allow.go Remove deny rules run.go Run command inside sandbox status.go Show current sandbox state reset.go Remove group, user, config doctor.go Check prerequisites and active isolation mode + help_ai.go Print AI-friendly usage reference + (serve is defined inline in main.go) helpers/ Logging and error types logger.go zerolog console logger errors.go Sentinel errors integration/ End-to-end CLI tests - cli_test.go Build binary, run real commands + cli_test.go Build binary, run real commands + sandbox_test.go Sandbox behaviour end-to-end + sandbox_escape_test.go Adversarial escape tests (-short skips them) ``` ## Key Design Decisions @@ -47,6 +59,7 @@ integration/ End-to-end CLI tests - **No CGO**: All platform operations use `exec.Command` to call system utilities (setfacl, groupadd, dscl, chmod, bwrap, slirp4netns, iptables/ip6tables). - **IPv6 sandbox is opt-in by capability detection**: `ipv6SandboxSupported()` requires both kernel v6 enabled and `ip6tables` on PATH. Either missing → sandbox runs IPv4-only. Partial v6 (NAT but no filter) is refused — it would silently bypass `allow_net`. - **Config merging**: Global config (`~/.aigate/config.yaml`) + project config (`.aigate.yaml`) merge with project extending global. +- **Audit log is append-only JSON Lines**: `AuditService` writes `run_started` / `blocked` events to `~/.aigate/audit.jsonl` under a mutex. `aigate serve` (`internal/web`) is a read-only dashboard over that file; it never mutates sandbox state. Keep the dashboard out of the sandbox enforcement path. ## Testing diff --git a/docs/user/README.md b/docs/user/README.md index 9f59fec..7e77a2e 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -81,7 +81,7 @@ curl -L https://github.com/AxeForging/aigate/releases/latest/download/aigate-dar sudo mv aigate-darwin-arm64 /usr/local/bin/aigate ``` -### From Source (Go 1.24+) +### From Source (Go 1.25+) ```sh go install github.com/AxeForging/aigate@latest ``` @@ -192,6 +192,17 @@ Example output: Isolation mode: bwrap + slirp4netns (full isolation) ``` +### serve + +Run a local web dashboard over the audit log. Read-only, binds to loopback by default — nothing is exposed off-host. + +```sh +aigate serve # http://127.0.0.1:8080 +aigate serve --addr 127.0.0.1:9000 # custom address (or AIGATE_ADDR env var) +``` + +The dashboard shows live counters and a timeline of `run_started` and `blocked` events drawn from `~/.aigate/audit.jsonl` (see [Audit log](#audit-log)). + ### reset Remove everything (group, user, config): @@ -217,27 +228,49 @@ deny_read: - "~/.ssh/" - "*.pem" - "*.key" + - "*.p12" - "~/.aws/" - "~/.gcloud/" - "~/.kube/config" - "~/.npmrc" - "~/.pypirc" + - "terraform.tfstate" + - "*.tfvars" deny_exec: - "curl" - "wget" - "nc" + - "ncat" + - "netcat" - "ssh" - "scp" + - "rsync" + - "ftp" - "kubectl delete" - "kubectl exec" allow_net: - "api.anthropic.com" - "api.openai.com" - "api.github.com" + - "registry.npmjs.org" + - "proxy.golang.org" resource_limits: max_memory: "4G" max_cpu_percent: 80 max_pids: 1000 +mask_stdout: + presets: + - openai + - anthropic + - aws_key + - aws_secret + - github + - bearer + patterns: + # Generic key=value / key: value assignments for common secret names + - regex: "(?:api_?key|secret|password|passwd|token|credential)\\s*[=:]\\s*\\S+" + show_prefix: 0 + case_insensitive: true ``` ### Output Masking (mask_stdout) @@ -251,6 +284,7 @@ resource_limits: | `openai` | `sk-...` / `sk-proj-...` | `sk-***` | | `anthropic` | `sk-ant-...` | `sk-ant-***` | | `aws_key` | `AKIA...` (access key ID) | `AKIA***` | +| `aws_secret` | `AWS_SECRET_ACCESS_KEY=...` assignment | `AWS_SECRET_ACCESS_KEY=***` | | `github` | `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_` | `ghp_***` | | `bearer` | `Bearer ` in headers/logs | `Bearer ***` | @@ -380,6 +414,20 @@ Without `bwrap`, aigate falls back to `unshare --user --map-root-user` + shell s Resource limits (`max_memory`, `max_cpu_percent`, `max_pids`) are defined in the config but **not yet enforced**. Enforcement via cgroups v2 controllers is planned for a future release. +## Audit log + +Each sandboxed run appends a line to `~/.aigate/audit.jsonl` (JSON Lines). Two kinds of events are recorded: + +- `run_started` — a sandbox launched, with the command and a count of active `deny_read` / `deny_exec` / `allow_net` / masking rules +- `blocked` — a command was refused by the `deny_exec` preflight check, with the matched rule + +```json +{"time":"2026-06-08T17:00:00Z","kind":"run_started","command":"claude","work_dir":"/home/me/proj","counts":{"deny_read":15,"deny_exec":11,"allow_net":5,"masking":7}} +{"time":"2026-06-08T17:01:12Z","kind":"blocked","rule":"curl","command":"curl ifconfig.me","source":"preflight"} +``` + +The file is plain text — `tail -f ~/.aigate/audit.jsonl` works, or run [`aigate serve`](#serve) for a live dashboard. + ## Troubleshooting ### "operation requires elevated privileges"