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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
curl -L https://github.com/AxeForging/aigate/releases/latest/download/aigate-linux-amd64.tar.gz | tar xz
sudo mv aigate-linux-amd64 /usr/local/bin/aigate

# Initialize sandbox
sudo aigate init
# Set up sandbox
sudo aigate setup # One-time: create OS group/user for ACLs
aigate init # Create default config

# Add restrictions
aigate deny read .env secrets/ *.pem
Expand Down Expand Up @@ -63,13 +64,15 @@ AI coding tools rely on application-level permission systems that can be bypasse
## Commands

```sh
aigate init # Create sandbox group/user/config
sudo aigate setup # Create OS group/user (one-time)
aigate init # Create default config
aigate deny read .env secrets/ *.pem # Block file access
aigate deny exec curl wget ssh # Block commands
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 help-ai # Show AI-friendly usage examples
aigate reset --force # Remove everything
```

Expand Down
139 changes: 139 additions & 0 deletions actions/help_ai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package actions

import (
"fmt"

"github.com/urfave/cli"
)

type HelpAIAction struct{}

func NewHelpAIAction() *HelpAIAction {
return &HelpAIAction{}
}

func (a *HelpAIAction) Execute(c *cli.Context) error {
fmt.Print(helpAIText)
return nil
}

const helpAIText = `aigate — AI Agent Usage Examples
=================================

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)

FILE RESTRICTIONS (deny read)
aigate deny read .env # Block a single file
aigate deny read .env secrets/ *.pem # Block multiple files/dirs/globs
aigate deny read .aws/ .gcloud/ .kube/ # Block cloud credential dirs
aigate deny read terraform.tfstate *.tfvars # Block Terraform state/secrets
aigate allow read .env # Remove a deny rule

COMMAND RESTRICTIONS (deny exec)
aigate deny exec curl wget # Block network tools
aigate deny exec ssh scp rsync # Block remote access tools
aigate deny exec nc ncat netcat # Block raw socket tools
aigate allow exec curl # Remove a deny rule

NETWORK RESTRICTIONS (deny net)
aigate deny net --except api.anthropic.com --except api.github.com
# Block all outbound except listed domains
aigate allow net api.github.com # Remove a domain from allow list

RUNNING SANDBOXED
aigate run -- claude # Run Claude in sandbox
aigate run -- aider # Run any AI tool
aigate run -- bash -c "npm test" # Run arbitrary commands
aigate run --config ./project.yaml -- claude # Use project-specific config
aigate run --verbose -- claude # Show debug logging

CHECKING STATUS
aigate status # Show all rules, group/user, limits

CONFIGURATION
Global config: ~/.aigate/config.yaml
Project config: .aigate.yaml (in project root, merged with global)

Project configs add to global rules (deny_read, deny_exec, allow_net are
unioned). Resource limits in project config override global values.

Example ~/.aigate/config.yaml:
group: ai-agents
user: ai-runner
deny_read:
- .env
- .env.*
- secrets/
- credentials/
- .ssh/
- "*.pem"
- "*.key"
deny_exec:
- curl
- wget
- ssh
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

Example .aigate.yaml (project-level, adds to global):
deny_read:
- .stripe-key
- production.env
allow_net:
- api.stripe.com

WHAT THE AI AGENT SEES INSIDE THE SANDBOX
Startup banner on stderr:
[aigate] sandbox active
[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)

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.

Denied directories contain a .aigate-denied marker file:
[aigate] access denied: this directory is protected by sandbox policy. Run 'cat /tmp/.aigate-policy' to see all active restrictions.

Policy summary file at /tmp/.aigate-policy lists all active restrictions.

Network connections to non-allowed hosts are rejected (connection refused).

COMMON PATTERNS
Node.js project:
aigate deny read .env .npmrc
aigate deny net --except registry.npmjs.org --except api.anthropic.com
aigate run -- claude

Python project:
aigate deny read .env .pypirc secrets/
aigate deny net --except pypi.org --except api.anthropic.com
aigate run -- claude

Go project:
aigate deny read .env *.key
aigate deny net --except proxy.golang.org --except api.anthropic.com
aigate run -- claude

Terraform project:
aigate deny read terraform.tfstate *.tfvars .aws/
aigate deny exec curl wget ssh
aigate run -- claude

Full lockdown:
aigate deny read .env .env.* secrets/ credentials/ .ssh/ .aws/ .gcloud/ *.pem *.key
aigate deny exec curl wget nc ncat netcat ssh scp rsync ftp
aigate deny net --except api.anthropic.com
aigate run -- claude
`
45 changes: 6 additions & 39 deletions actions/init.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package actions

import (
"errors"
"fmt"

"github.com/AxeForging/aigate/helpers"
Expand All @@ -10,65 +9,33 @@ import (
)

type InitAction struct {
platform services.Platform
configSvc *services.ConfigService
}

func NewInitAction(p services.Platform, c *services.ConfigService) *InitAction {
return &InitAction{platform: p, configSvc: c}
func NewInitAction(c *services.ConfigService) *InitAction {
return &InitAction{configSvc: c}
}

func (a *InitAction) Execute(c *cli.Context) error {
if c.Bool("verbose") {
helpers.SetupLogger("debug")
}

group := c.String("group")
user := c.String("user")

if !c.Bool("force") && a.configSvc.ConfigExists() {
return fmt.Errorf("%w: use --force to reinitialize", helpers.ErrAlreadyInit)
}

helpers.Log.Info().Str("platform", a.platform.Name()).Msg("Initializing aigate sandbox")

// Create group — skip if already exists
helpers.Log.Info().Str("group", group).Msg("Creating sandbox group")
if err := a.platform.CreateGroup(group); err != nil {
if errors.Is(err, helpers.ErrAlreadyInit) {
helpers.Log.Info().Str("group", group).Msg("Sandbox group already exists, skipping")
} else {
return fmt.Errorf("failed to create group: %w", err)
}
}

// Create user — skip if already exists
helpers.Log.Info().Str("user", user).Str("group", group).Msg("Creating sandbox user")
if err := a.platform.CreateUser(user, group); err != nil {
if errors.Is(err, helpers.ErrAlreadyInit) {
helpers.Log.Info().Str("user", user).Msg("Sandbox user already exists, skipping")
} else {
return fmt.Errorf("failed to create user: %w", err)
}
}

// Write default config
cfg := a.configSvc.InitDefaultConfig()
cfg.Group = group
cfg.User = user
if err := a.configSvc.SaveGlobal(cfg); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}

configPath, _ := a.configSvc.GlobalConfigPath()
helpers.Log.Info().Str("config", configPath).Msg("Default config created")
fmt.Printf("aigate initialized successfully\n")
fmt.Printf(" Group: %s\n", group)
fmt.Printf(" User: %s\n", user)
fmt.Printf("aigate initialized\n")
fmt.Printf(" Config: %s\n", configPath)
fmt.Printf("\nNext steps:\n")
fmt.Printf(" aigate deny read .env secrets/ # Add file restrictions\n")
fmt.Printf(" aigate deny exec curl wget # Block commands\n")
fmt.Printf(" aigate run -- claude # Run AI tool in sandbox\n")
fmt.Printf(" sudo aigate setup # Create OS group/user (one-time)\n")
fmt.Printf(" aigate deny read .env secrets/ # Add file restrictions\n")
fmt.Printf(" aigate run -- claude # Run AI tool in sandbox\n")
return nil
}
55 changes: 55 additions & 0 deletions actions/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package actions

import (
"errors"
"fmt"

"github.com/AxeForging/aigate/helpers"
"github.com/AxeForging/aigate/services"
"github.com/urfave/cli"
)

type SetupAction struct {
platform services.Platform
configSvc *services.ConfigService
}

func NewSetupAction(p services.Platform, c *services.ConfigService) *SetupAction {
return &SetupAction{platform: p, configSvc: c}
}

func (a *SetupAction) Execute(c *cli.Context) error {
if c.Bool("verbose") {
helpers.SetupLogger("debug")
}

group := c.String("group")
user := c.String("user")

helpers.Log.Info().Str("platform", a.platform.Name()).Msg("Setting up aigate system resources")

// Create group — skip if already exists
helpers.Log.Info().Str("group", group).Msg("Creating sandbox group")
if err := a.platform.CreateGroup(group); err != nil {
if errors.Is(err, helpers.ErrAlreadyInit) {
helpers.Log.Info().Str("group", group).Msg("group already exists, skipping")
} else {
return fmt.Errorf("failed to create group: %w", err)
}
}

// Create user — skip if already exists
helpers.Log.Info().Str("user", user).Str("group", group).Msg("Creating sandbox user")
if err := a.platform.CreateUser(user, group); err != nil {
if errors.Is(err, helpers.ErrAlreadyInit) {
helpers.Log.Info().Str("user", user).Msg("user already exists, skipping")
} else {
return fmt.Errorf("failed to create user: %w", err)
}
}

fmt.Printf("System setup complete\n")
fmt.Printf(" Group: %s\n", group)
fmt.Printf(" User: %s\n", user)
return nil
}
33 changes: 22 additions & 11 deletions docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,29 +60,40 @@ go install github.com/AxeForging/aigate@latest
## Quick Start

```sh
# 1. Initialize (creates OS group + user + default config)
sudo aigate init
# 1. System setup (creates OS group + user, requires sudo)
sudo aigate setup

# 2. Add custom restrictions
# 2. Create default config
aigate init

# 3. Add custom restrictions
aigate deny read .env secrets/ terraform.tfstate
aigate deny exec curl wget ssh

# 3. Run your AI tool inside the sandbox
# 4. Run your AI tool inside the sandbox
aigate run -- claude
aigate run -- cursor
aigate run -- aider
```

## Commands

### setup

Creates the OS group (`ai-agents`) and user (`ai-runner`). Requires `sudo`. Safe to re-run (skips existing group/user).

```sh
sudo aigate setup # Default group/user
sudo aigate setup --group mygroup --user myuser # Custom names
```

### init

Creates the sandbox group (`ai-agents`), user (`ai-runner`), and default config. Safe to re-run (skips existing group/user).
Creates default config at `~/.aigate/config.yaml`. Does not require sudo.

```sh
sudo aigate init # Default setup
sudo aigate init --group mygroup --user myuser # Custom names
sudo aigate init --force # Reinitialize
aigate init # Create default config
aigate init --force # Re-create config (overwrites existing)
```

### deny
Expand Down Expand Up @@ -246,13 +257,13 @@ cgroups v2 enforce memory, CPU, and PID limits (Linux only).
## Troubleshooting

### "operation requires elevated privileges"
`init` and `reset` need `sudo` to create/delete OS users and groups. `deny`, `allow`, `run`, and `status` do not.
`setup` and `reset` need `sudo` to create/delete OS users and groups. `init`, `deny`, `allow`, `run`, and `status` do not.

### ACL warnings on deny/allow
If you see "Failed to apply ACLs", the AI agent group may not exist yet. Run `sudo aigate init` first.
If you see "Failed to apply ACLs", the AI agent group may not exist yet. Run `sudo aigate setup` first.

### "aigate not initialized"
Run `sudo aigate init` to create the sandbox group, user, and default config.
Run `sudo aigate setup` to create the sandbox group and user, then `aigate init` to create the default config.

### "slirp4netns not found" warning
Install `slirp4netns` for network filtering on Linux (see [Prerequisites](#prerequisites)). Without it, `allow_net` rules are ignored and the sandboxed process has unrestricted network access.
Expand Down
Loading