Skip to content
Open
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
74 changes: 74 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: PR

on:
pull_request:
types: [opened, edited, synchronize, reopened]
branches:
- main

permissions:
contents: read
pull-requests: write

jobs:
title:
runs-on: ubuntu-latest
steps:
- name: Validate PR title follows Conventional Commits
env:
TITLE: ${{ github.event.pull_request.title }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if echo "$TITLE" | grep -qE "^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?(!)?: .+"; then
echo "PR title is valid: $TITLE"
exit 0
fi

BODY=$(cat <<'COMMENT'
### ⚠️ Invalid PR Title

PR title must follow the **Conventional Commits** format since we use squash merge:

```
<type>[optional scope][!]: <description>
```

**Allowed types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `build`, `ci`, `perf`, `revert`

**Examples:**
- `feat: add new feature`
- `fix(sandbox): resolve namespace issue`
- `feat!: breaking change`
- `chore(deps): update dependencies`
COMMENT
)

gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
-X POST -f body="$BODY"

echo "::error::PR title must follow Conventional Commits format"
exit 1

review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: AxeForging/reviewforge@main
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AI_PROVIDER: gemini
AI_MODEL: gemini-2.5-flash
AI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
SHOW_TOKEN_USAGE: true
INCREMENTAL: false
REVIEW_RULES: concise

validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: AxeForging/structlint@main
with:
config: .structlint.yaml
comment-on-pr: "true"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 changes: 35 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g. v1.0.0)'
required: true
description: 'Release tag (leave empty for auto-bump from conventional commits)'
required: false
type: string

permissions:
contents: write
Expand All @@ -14,7 +15,8 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

Expand All @@ -27,16 +29,39 @@ jobs:
- name: Run tests
run: go test ./... -v

- name: Determine version
id: version
uses: AxeForging/releaseforge@main
with:
command: bump

- name: Set tag
id: tag
run: |
if [ -n "${{ inputs.tag }}" ]; then
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${{ steps.version.outputs.next-version }}" >> "$GITHUB_OUTPUT"
fi

- name: Generate release notes
id: notes
uses: AxeForging/releaseforge@main
with:
command: generate
api-key: ${{ secrets.GEMINI_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Create and push tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a ${{ github.event.inputs.tag }} -m "Release ${{ github.event.inputs.tag }}"
git push origin ${{ github.event.inputs.tag }}
echo "Releasing ${{ steps.tag.outputs.tag }}"
git tag ${{ steps.tag.outputs.tag }}
git push origin ${{ steps.tag.outputs.tag }}

- uses: goreleaser/goreleaser-action@v6
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: "release --clean"
args: release --clean --release-notes ${{ steps.notes.outputs.release-notes }}
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 changes: 20 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Test

on:
push:
branches: [ "**" ]
pull_request:

jobs:
Expand All @@ -22,3 +20,23 @@ jobs:

- name: Run tests
run: go test ./... -v

- name: Build binary
run: make build-local

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=5m
88 changes: 88 additions & 0 deletions .structlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
dir_structure:
allowedPaths:
- "."
- "actions/**"
- "domain/**"
- "services/**"
- "helpers/**"
- "integration/**"
- "docs/**"
- "dist/**"
- ".github/**"
- ".claude/**"
disallowedPaths:
- "vendor/**"
- "node_modules/**"
- "tmp/**"
- "temp/**"
- ".git/**"
- "*.log"
requiredPaths:
- "actions"
- "domain"
- "services"
- "helpers"
- "docs"

file_naming_pattern:
allowed:
- "*.go"
- "*.mod"
- "*.sum"
- "*.yaml"
- "*.yml"
- "*.json"
- "*.toml"
- "*.md"
- "*.txt"
- "*.png"
- "*.jpg"
- "*.svg"
- "*.puml"
- "README*"
- "LICENSE*"
- "CHANGELOG*"
- "Makefile"
- "Dockerfile*"
- "*.sh"
- ".gitignore"
- ".editorconfig"
- ".golangci.yml"
- ".goreleaser.yaml"
- ".goreleaser.yml"
- ".github/**"
disallowed:
- "*.env*"
- ".env*"
- "*.key"
- "*.pem"
- "*.p12"
- "*.log"
- "*.tmp"
- "*.temp"
- "*~"
- "*.swp"
- "*.swo"
- "*.bak"
- "*.backup"
- ".DS_Store"
- "Thumbs.db"
required:
- "go.mod"
- "README.md"
- ".gitignore"
- "main.go"

ignore:
- ".git"
- "vendor"
- "node_modules"
- "bin"
- "dist"
- "build"
- ".idea"
- ".vscode"
- ".DS_Store"
- "*.log"
- "*.tmp"
- "aigate"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ AI coding tools rely on application-level permission systems that can be bypasse
- **Process isolation** - Mount namespaces overmount sensitive directories (Linux)
- **Network isolation** - Network namespaces 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)
- **Tool-agnostic** - Works with any AI tool: Claude Code, Cursor, Copilot, Aider
- **Sensible defaults** - Ships with deny rules for .env, secrets/, .ssh/, *.pem, etc.
Expand Down
48 changes: 48 additions & 0 deletions actions/help_ai.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,68 @@ CONFIGURATION
max_memory: 4G
max_cpu_percent: 80
max_pids: 1000
mask_stdout:
presets:
- openai
- anthropic
- aws_key
- github
- bearer

Example .aigate.yaml (project-level, adds to global):
deny_read:
- .stripe-key
- production.env
allow_net:
- api.stripe.com
mask_stdout:
presets:
- openai
- bearer
patterns:
- regex: "myapp-secret-[a-z0-9]+"
show_prefix: 0
case_insensitive: false
- regex: "(?:db_pass|database_password)\\s*[=:]\\s*\\S+"
show_prefix: 0
case_insensitive: true

OUTPUT MASKING (mask_stdout)
Redacts secrets from stdout/stderr before they reach the terminal. Applied in
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 <token> → Bearer ***

All 5 presets are enabled by default (aigate init).

Pattern options:
regex RE2-compatible regular expression (required)
show_prefix bytes to preserve before *** (default: 0, fully masked)
case_insensitive match regardless of letter case (default: false)

Custom pattern examples:
mask_stdout:
patterns:
- regex: "mysecret-[a-z0-9]+"
show_prefix: 0 # → ***
- regex: "token-[a-zA-Z0-9]{16}"
show_prefix: 6 # → token-***
- regex: "(?:password|secret)\\s*[=:]\\s*\\S+"
show_prefix: 0
case_insensitive: true # catches PASSWORD=, Password=, etc.

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)
[aigate] mask_stdout: openai, anthropic, aws_key, 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.
Expand Down
10 changes: 10 additions & 0 deletions actions/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,14 @@ func printSandboxBanner(cfg *domain.Config) {
if len(cfg.AllowNet) > 0 {
fmt.Fprintf(os.Stderr, "[aigate] allow_net: %s (all other outbound connections will be blocked)\n", strings.Join(cfg.AllowNet, ", "))
}
if len(cfg.MaskStdout.Presets) > 0 || len(cfg.MaskStdout.Patterns) > 0 {
var parts []string
if len(cfg.MaskStdout.Presets) > 0 {
parts = append(parts, strings.Join(cfg.MaskStdout.Presets, ", "))
}
if len(cfg.MaskStdout.Patterns) > 0 {
parts = append(parts, fmt.Sprintf("+%d custom pattern(s)", len(cfg.MaskStdout.Patterns)))
}
fmt.Fprintf(os.Stderr, "[aigate] mask_stdout: %s\n", strings.Join(parts, "; "))
}
}
Loading