diff --git a/README.md b/README.md
index 69b72ed..d52fd8d 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
-
+
@@ -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"