Introduce built-in templates for enhanced configuration options, support JSONC format

This commit is contained in:
JY Tan
2025-12-28 22:16:50 -08:00
parent 8317bb96bc
commit d8e55d9515
22 changed files with 655 additions and 83 deletions

View File

@@ -14,6 +14,7 @@ import (
"github.com/Use-Tusk/fence/internal/config"
"github.com/Use-Tusk/fence/internal/platform"
"github.com/Use-Tusk/fence/internal/sandbox"
"github.com/Use-Tusk/fence/internal/templates"
"github.com/spf13/cobra"
)
@@ -28,6 +29,8 @@ var (
debug bool
monitor bool
settingsPath string
templateName string
listTemplates bool
cmdString string
exposePorts []string
exitCode int
@@ -50,15 +53,18 @@ func main() {
with network and filesystem restrictions.
By default, all network access is blocked. Configure allowed domains in
~/.fence.json or pass a settings file with --settings.
~/.fence.json or pass a settings file with --settings, or use a built-in
template with --template.
Examples:
fence curl https://example.com # Will be blocked (no domains allowed)
fence -- curl -s https://example.com # Use -- to separate fence flags from command
fence -c "echo hello && ls" # Run with shell expansion
fence --settings config.json npm install
fence -t npm-install npm install # Use built-in npm-install template
fence -t ai-coding-agents -- agent-cmd # Use AI coding agents template
fence -p 3000 -c "npm run dev" # Expose port 3000 for inbound connections
fence -p 3000 -p 8080 -c "npm start" # Expose multiple ports
fence --list-templates # Show available built-in templates
Configuration file format (~/.fence.json):
{
@@ -84,6 +90,8 @@ Configuration file format (~/.fence.json):
rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging")
rootCmd.Flags().BoolVarP(&monitor, "monitor", "m", false, "Monitor and log sandbox violations (macOS: log stream, all: proxy denials)")
rootCmd.Flags().StringVarP(&settingsPath, "settings", "s", "", "Path to settings file (default: ~/.fence.json)")
rootCmd.Flags().StringVarP(&templateName, "template", "t", "", "Use built-in template (e.g., ai-coding-agents, npm-install)")
rootCmd.Flags().BoolVar(&listTemplates, "list-templates", false, "List available templates")
rootCmd.Flags().StringVarP(&cmdString, "c", "c", "", "Run command string directly (like sh -c)")
rootCmd.Flags().StringArrayVarP(&exposePorts, "port", "p", nil, "Expose port for inbound connections (can be used multiple times)")
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information")
@@ -112,6 +120,11 @@ func runCommand(cmd *cobra.Command, args []string) error {
return nil
}
if listTemplates {
printTemplates()
return nil
}
var command string
switch {
case cmdString != "":
@@ -139,21 +152,36 @@ func runCommand(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "[fence] Exposing ports: %v\n", ports)
}
configPath := settingsPath
if configPath == "" {
configPath = config.DefaultConfigPath()
}
// Load config: template > settings file > default path
var cfg *config.Config
var err error
cfg, err := config.Load(configPath)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if cfg == nil {
if debug {
fmt.Fprintf(os.Stderr, "[fence] No config found at %s, using default (block all network)\n", configPath)
switch {
case templateName != "":
cfg, err = templates.Load(templateName)
if err != nil {
return fmt.Errorf("failed to load template: %w\nUse --list-templates to see available templates", err)
}
if debug {
fmt.Fprintf(os.Stderr, "[fence] Using template: %s\n", templateName)
}
case settingsPath != "":
cfg, err = config.Load(settingsPath)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
default:
configPath := config.DefaultConfigPath()
cfg, err = config.Load(configPath)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if cfg == nil {
if debug {
fmt.Fprintf(os.Stderr, "[fence] No config found at %s, using default (block all network)\n", configPath)
}
cfg = config.Default()
}
cfg = config.Default()
}
manager := sandbox.NewManager(cfg, debug, monitor)
@@ -226,11 +254,19 @@ func runCommand(cmd *cobra.Command, args []string) error {
// Landlock code exists for future integration (e.g., via a wrapper binary).
go func() {
sig := <-sigChan
if execCmd.Process != nil {
_ = execCmd.Process.Signal(sig)
sigCount := 0
for sig := range sigChan {
sigCount++
if execCmd.Process == nil {
continue
}
// First signal: graceful termination; second signal: force kill
if sigCount >= 2 {
_ = execCmd.Process.Kill()
} else {
_ = execCmd.Process.Signal(sig)
}
}
// Give child time to exit, then cleanup will happen via defer
}()
// Wait for command to finish
@@ -246,6 +282,18 @@ func runCommand(cmd *cobra.Command, args []string) error {
return nil
}
// printTemplates prints all available templates to stdout.
func printTemplates() {
fmt.Println("Available templates:")
fmt.Println()
for _, t := range templates.List() {
fmt.Printf(" %-20s %s\n", t.Name, t.Description)
}
fmt.Println()
fmt.Println("Usage: fence -t <template> <command>")
fmt.Println("Example: fence -t code -- code")
}
// runLandlockWrapper runs in "wrapper mode" inside the sandbox.
// It applies Landlock restrictions and then execs the user command.
// Usage: fence --landlock-apply [--debug] -- <command...>

View File

@@ -13,7 +13,7 @@ Fence is a sandboxing tool that restricts network and filesystem access for arbi
- [Troubleshooting](troubleshooting.md) - Common failure modes and fixes
- [Using Fence with AI agents](agents.md) - Defense-in-depth and policy standardization
- [Recipes](recipes/README.md) - Common workflows (npm/pip/git/CI)
- [Config Templates](templates/) - Copy/paste templates you can start from
- [Templates](./templates.md) - Copy/paste templates you can start from
## Reference

View File

@@ -1,6 +1,6 @@
# Using Fence with AI Agents
Many popular coding agents already include sandboxing. Fence can still be useful when you want a **tool-agnostic** policy layer that works the same way across:
Many popular coding agents already include sandboxing. Fence can still be useful when you want a tool-agnostic policy layer that works the same way across:
- local developer machines
- CI jobs
@@ -11,11 +11,11 @@ Many popular coding agents already include sandboxing. Fence can still be useful
Treat an agent as "semi-trusted automation":
- **Restrict writes** to the workspace (and maybe `/tmp`)
- **Allowlist only the network destinations** you actually need
- Restrict writes to the workspace (and maybe `/tmp`)
- Allowlist only the network destinations you actually need
- Use `-m` (monitor mode) to audit blocked attempts and tighten policy
Fence can also reduce the risk of running agents with fewer interactive permission prompts (e.g. "skip permissions"), **as long as your Fence config tightly scopes writes and outbound destinations**. It's defense-in-depth, not a substitute for the agent's own safeguards.
Fence can also reduce the risk of running agents with fewer interactive permission prompts (e.g. "skip permissions"), as long as your Fence config tightly scopes writes and outbound destinations. It's defense-in-depth, not a substitute for the agent's own safeguards.
## Example: API-only agent
@@ -36,6 +36,18 @@ Run:
fence --settings ./fence.json <agent-command>
```
## Real-world usage
Currently, we provide the `code.json` template. You can use it by running `fence -t code -- claude`.
However, not all coding agent CLIs work with Fence yet. We're actively investigating these issues.
| Agent | Works? | Notes |
|-------|--------| ----- |
| Claude Code | ✅ | Fully working with `code` template |
| Codex | ❌ | Missing unidentified sandbox permission for interactive mode |
| OpenCode | ❌ | Ignores proxy env vars; makes direct network connections |
## Protecting your environment
Fence includes additional "dangerous file protection (writes blocked regardless of config) to reduce persistence and environment-tampering vectors like:
@@ -44,4 +56,4 @@ Fence includes additional "dangerous file protection (writes blocked regardless
- shell startup files (`.zshrc`, `.bashrc`, etc.)
- some editor/tool config directories
See `ARCHITECTURE.md` for the full list and rationale.
See [`ARCHITECTURE.md`](/ARCHITECTURE.md) for the full list and rationale.

View File

@@ -1,6 +1,6 @@
# Configuration
Fence reads settings from `~/.fence.json` by default (or pass `--settings ./fence.json`).
Fence reads settings from `~/.fence.json` by default (or pass `--settings ./fence.json`). Config files support JSONC.
Example config:

28
docs/templates.md Normal file
View File

@@ -0,0 +1,28 @@
# Config Templates
Fence includes built-in config templates for common use cases. Templates are embedded in the binary, so you can use them directly without copying files.
## Using templates
Use the `-t` / `--template` flag to apply a template:
```bash
# Use a built-in template
fence -t npm-install npm install
# Wraps Claude Code
fence -t code -- claude
# List available templates
fence --list-templates
```
You can also copy and customize templates from [`internal/templates/`](/internal/templates/).
## Available Templates
| Template | Description |
|----------|-------------|
| `code` | Production-ready config for AI coding agents (Claude Code, Codex, Copilot, etc.) |
| `git-readonly` | Blocks destructive commands like `git push`, `rm -rf`, etc. |
| `local-dev-server` | Allow binding and localhost outbound; allow writes to workspace/tmp |

View File

@@ -1,19 +0,0 @@
# Config Templates
This directory contains Fence config templates. They are small and meant to be copied and customized.
## Templates
- `default-deny.json`: no network allowlist; no write access (most restrictive)
- `workspace-write.json`: allow writes in the current directory
- `npm-install.json`: allow npm registry; allow writes to workspace/node_modules/tmp
- `pip-install.json`: allow PyPI; allow writes to workspace/tmp
- `local-dev-server.json`: allow binding and localhost outbound; allow writes to workspace/tmp
- `agent-api-only.json`: allow common LLM API domains; allow writes to workspace
- `git-readonly.json`: blocks destructive commands like `git push`, `rm -rf`, etc.
## Using a template
```bash
fence --settings ./docs/templates/npm-install.json npm install
```

View File

@@ -1,8 +0,0 @@
{
"network": {
"allowedDomains": ["api.openai.com", "api.anthropic.com"]
},
"filesystem": {
"allowWrite": ["."]
}
}

View File

@@ -1,8 +0,0 @@
{
"network": {
"allowedDomains": []
},
"filesystem": {
"allowWrite": []
}
}

View File

@@ -1,8 +0,0 @@
{
"network": {
"allowedDomains": ["registry.npmjs.org", "*.npmjs.org"]
},
"filesystem": {
"allowWrite": [".", "node_modules", "/tmp"]
}
}

View File

@@ -1,8 +0,0 @@
{
"network": {
"allowedDomains": ["pypi.org", "files.pythonhosted.org"]
},
"filesystem": {
"allowWrite": [".", "/tmp"]
}
}

View File

@@ -1,5 +0,0 @@
{
"filesystem": {
"allowWrite": ["."]
}
}

1
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/bmatcuk/doublestar/v4 v4.9.1
github.com/spf13/cobra v1.8.1
github.com/things-go/go-socks5 v0.0.5
github.com/tidwall/jsonc v0.3.2
golang.org/x/sys v0.39.0
)

2
go.sum
View File

@@ -16,6 +16,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/things-go/go-socks5 v0.0.5 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8=
github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ=
github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=

View File

@@ -9,6 +9,8 @@ import (
"path/filepath"
"slices"
"strings"
"github.com/tidwall/jsonc"
)
// Config is the main configuration for fence.
@@ -134,7 +136,7 @@ func Load(path string) (*Config, error) {
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
if err := json.Unmarshal(jsonc.ToJSON(data), &cfg); err != nil {
return nil, fmt.Errorf("invalid JSON in config file: %w", err)
}
@@ -231,6 +233,11 @@ func MatchesDomain(hostname, pattern string) bool {
hostname = strings.ToLower(hostname)
pattern = strings.ToLower(pattern)
// "*" matches all domains
if pattern == "*" {
return true
}
// Wildcard pattern like *.example.com
if strings.HasPrefix(pattern, "*.") {
baseDomain := pattern[2:]

View File

@@ -170,6 +170,41 @@ func TestCreateDomainFilter(t *testing.T) {
port: 443,
allowed: false,
},
{
name: "star wildcard allows all",
cfg: &config.Config{
Network: config.NetworkConfig{
AllowedDomains: []string{"*"},
},
},
host: "any-domain.example.com",
port: 443,
allowed: true,
},
{
name: "star wildcard with deny list",
cfg: &config.Config{
Network: config.NetworkConfig{
AllowedDomains: []string{"*"},
DeniedDomains: []string{"blocked.com"},
},
},
host: "blocked.com",
port: 443,
allowed: false,
},
{
name: "star wildcard allows non-denied",
cfg: &config.Config{
Network: config.NetworkConfig{
AllowedDomains: []string{"*"},
DeniedDomains: []string{"blocked.com"},
},
},
host: "allowed.com",
port: 443,
allowed: true,
},
}
for _, tt := range tests {

View File

@@ -307,6 +307,9 @@ func GenerateSandboxProfile(params MacOSSandboxParams) string {
(global-name "com.apple.bsd.dirhelper")
(global-name "com.apple.securityd.xpc")
(global-name "com.apple.coreservices.launchservicesd")
(global-name "com.apple.FSEvents")
(global-name "com.apple.fseventsd")
(global-name "com.apple.SystemConfiguration.configd")
)
; POSIX IPC

View File

@@ -0,0 +1,157 @@
{
"allowPty": true,
"network": {
"allowLocalBinding": true,
"allowLocalOutbound": true,
"allowedDomains": [
// LLM API providers
"api.openai.com",
"*.anthropic.com",
"api.githubcopilot.com",
"generativelanguage.googleapis.com",
"api.mistral.ai",
"api.cohere.ai",
"api.together.xyz",
"openrouter.ai",
// Git hosting
"github.com",
"api.github.com",
"raw.githubusercontent.com",
"codeload.github.com",
"objects.githubusercontent.com",
"gitlab.com",
// Package registries
"registry.npmjs.org",
"*.npmjs.org",
"registry.yarnpkg.com",
"pypi.org",
"files.pythonhosted.org",
"crates.io",
"static.crates.io",
"index.crates.io",
"proxy.golang.org",
"sum.golang.org",
// Model registry
"models.dev"
],
"deniedDomains": [
// Cloud metadata APIs (prevent credential theft)
"169.254.169.254",
"metadata.google.internal",
"instance-data.ec2.internal",
// Telemetry (optional, can be removed if needed)
"statsig.anthropic.com",
"*.sentry.io"
]
},
"filesystem": {
"allowWrite": [
".",
// Temp files
"/tmp",
// Claude Code state/config
"~/.claude*",
"~/.claude/**",
// Codex state/config
"~/.codex/**",
// Package manager caches
"~/.npm/_cacache",
"~/.cache",
"~/.bun/**",
// Cargo cache (Rust, used by Codex)
"~/.cargo/registry/**",
"~/.cargo/git/**",
"~/.cargo/.package-cache",
// Shell completion cache
"~/.zcompdump*",
// XDG directories for app configs/data
"~/.local/share/**",
"~/.config/**",
// OpenCode state
"~/.opencode/**"
],
"denyWrite": [
// Protect environment files with secrets
".env",
".env.*",
"**/.env",
"**/.env.*",
// Protect key/certificate files
"*.key",
"*.pem",
"*.p12",
"*.pfx",
"**/*.key",
"**/*.pem",
"**/*.p12",
"**/*.pfx"
],
"denyRead": [
// SSH private keys and config
"~/.ssh/id_*",
"~/.ssh/config",
"~/.ssh/*.pem",
// GPG keys
"~/.gnupg/**",
// Cloud provider credentials
"~/.aws/**",
"~/.config/gcloud/**",
"~/.kube/**",
// Docker config (may contain registry auth)
"~/.docker/**",
// GitHub CLI auth
"~/.config/gh/**",
// Package manager auth tokens
"~/.pypirc",
"~/.netrc",
"~/.git-credentials",
"~/.cargo/credentials",
"~/.cargo/credentials.toml"
]
},
"command": {
"useDefaults": true,
"deny": [
// Git commands that modify remote state
"git push",
"git reset",
"git clean",
"git checkout --",
"git rebase",
"git merge",
// Package publishing commands
"npm publish",
"pnpm publish",
"yarn publish",
"cargo publish",
"twine upload",
"gem push",
// Privilege escalation
"sudo"
]
}
}

View File

@@ -0,0 +1,116 @@
{
"allowPty": true,
"network": {
"allowLocalBinding": true,
"allowLocalOutbound": true,
"allowedDomains": ["*"],
// Block common analytics, telemetry, and error reporting services
"deniedDomains": [
// Error reporting
"*.sentry.io",
"*.ingest.sentry.io",
"sentry.io",
// Product analytics
"*.posthog.com",
"app.posthog.com",
"us.posthog.com",
"eu.posthog.com",
// Feature flags / experimentation
"*.statsig.com",
"statsig.com",
"statsig.anthropic.com",
// Customer data platforms
"*.segment.io",
"*.segment.com",
"api.segment.io",
"cdn.segment.com",
// Analytics
"*.amplitude.com",
"api.amplitude.com",
"api2.amplitude.com",
"*.mixpanel.com",
"api.mixpanel.com",
"*.heap.io",
"*.heapanalytics.com",
// Session recording
"*.fullstory.com",
"*.hotjar.com",
"*.hotjar.io",
"*.logrocket.io",
"*.logrocket.com",
// Error tracking
"*.bugsnag.com",
"notify.bugsnag.com",
"*.rollbar.com",
"api.rollbar.com",
// APM / Monitoring
"*.datadog.com",
"*.datadoghq.com",
"*.newrelic.com",
"*.nr-data.net",
// Feature flags
"*.launchdarkly.com",
"*.split.io",
// Product analytics / user engagement
"*.pendo.io",
"*.intercom.io",
"*.intercom.com",
// Mobile attribution
"*.appsflyer.com",
"*.adjust.com",
"*.branch.io",
// Crash reporting
"crashlytics.com",
"*.crashlytics.com",
"firebase-settings.crashlytics.com"
]
},
"filesystem": {
"allowWrite": [
".",
"/tmp",
// Claude Code state/config
"~/.claude*",
"~/.claude/**",
// Codex state/config
"~/.codex/**",
// Package manager caches
"~/.npm/_cacache",
"~/.cache",
"~/.bun/**",
// Cargo cache (Rust, used by Codex)
"~/.cargo/registry/**",
"~/.cargo/git/**",
"~/.cargo/.package-cache",
// Shell completion cache
"~/.zcompdump*",
// XDG directories for app configs/data
"~/.local/share/**",
"~/.config/**",
// OpenCode state
"~/.opencode/**"
]
}
}

View File

@@ -0,0 +1,96 @@
// Package templates provides embedded configuration templates for fence.
package templates
import (
"embed"
"encoding/json"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/Use-Tusk/fence/internal/config"
"github.com/tidwall/jsonc"
)
//go:embed *.json
var templatesFS embed.FS
// Template represents a named configuration template.
type Template struct {
Name string
Description string
}
// AvailableTemplates lists all embedded templates with descriptions.
var templateDescriptions = map[string]string{
"default-deny": "No network allowlist; no write access (most restrictive)",
"disable-telemetry": "Block analytics/error reporting (Sentry, Posthog, Statsig, etc.)",
"workspace-write": "Allow writes in the current directory",
"npm-install": "Allow npm registry; allow writes to workspace/node_modules/tmp",
"pip-install": "Allow PyPI; allow writes to workspace/tmp",
"local-dev-server": "Allow binding and localhost outbound; allow writes to workspace/tmp",
"git-readonly": "Blocks destructive commands like git push, rm -rf, etc.",
"code": "Production-ready config for AI coding agents (Claude Code, Codex, Copilot, etc.)",
}
// List returns all available template names sorted alphabetically.
func List() []Template {
entries, err := templatesFS.ReadDir(".")
if err != nil {
return nil
}
var templates []Template
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
name := strings.TrimSuffix(entry.Name(), ".json")
desc := templateDescriptions[name]
if desc == "" {
desc = "No description available"
}
templates = append(templates, Template{Name: name, Description: desc})
}
sort.Slice(templates, func(i, j int) bool {
return templates[i].Name < templates[j].Name
})
return templates
}
// Load loads a template by name and returns the parsed config.
func Load(name string) (*config.Config, error) {
// Normalize name (remove .json if present)
name = strings.TrimSuffix(name, ".json")
filename := name + ".json"
data, err := templatesFS.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("template %q not found", name)
}
var cfg config.Config
if err := json.Unmarshal(jsonc.ToJSON(data), &cfg); err != nil {
return nil, fmt.Errorf("failed to parse template %q: %w", name, err)
}
return &cfg, nil
}
// Exists checks if a template with the given name exists.
func Exists(name string) bool {
name = strings.TrimSuffix(name, ".json")
filename := name + ".json"
_, err := templatesFS.ReadFile(filename)
return err == nil
}
// GetPath returns the embedded path for a template (for display purposes).
func GetPath(name string) string {
name = strings.TrimSuffix(name, ".json")
return filepath.Join("internal/templates", name+".json")
}

View File

@@ -0,0 +1,123 @@
package templates
import (
"testing"
)
func TestList(t *testing.T) {
templates := List()
if len(templates) == 0 {
t.Fatal("expected at least one template")
}
// Check that code template exists
found := false
for _, tmpl := range templates {
if tmpl.Name == "code" {
found = true
if tmpl.Description == "" {
t.Error("code template should have a description")
}
break
}
}
if !found {
t.Error("code template not found")
}
}
func TestLoad(t *testing.T) {
tests := []struct {
name string
wantErr bool
}{
{"code", false},
{"disable-telemetry", false},
{"git-readonly", false},
{"local-dev-server", false},
{"nonexistent", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := Load(tt.name)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if cfg == nil {
t.Error("expected config, got nil")
}
}
})
}
}
func TestLoadWithJsonExtension(t *testing.T) {
// Should work with or without .json extension
cfg1, err := Load("disable-telemetry")
if err != nil {
t.Fatalf("failed to load disable-telemetry: %v", err)
}
cfg2, err := Load("disable-telemetry.json")
if err != nil {
t.Fatalf("failed to load disable-telemetry.json: %v", err)
}
// Both should return valid configs
if cfg1 == nil || cfg2 == nil {
t.Error("expected both configs to be non-nil")
}
}
func TestExists(t *testing.T) {
if !Exists("code") {
t.Error("code template should exist")
}
if Exists("nonexistent") {
t.Error("nonexistent should not exist")
}
}
func TestCodeTemplate(t *testing.T) {
cfg, err := Load("code")
if err != nil {
t.Fatalf("failed to load code template: %v", err)
}
// Verify key settings
if !cfg.AllowPty {
t.Error("code template should have AllowPty=true")
}
if len(cfg.Network.AllowedDomains) == 0 {
t.Error("code template should have allowed domains")
}
// Check that *.anthropic.com is in allowed domains
found := false
for _, domain := range cfg.Network.AllowedDomains {
if domain == "*.anthropic.com" {
found = true
break
}
}
if !found {
t.Error("*.anthropic.com should be in allowed domains")
}
// Check that cloud metadata domains are denied
if len(cfg.Network.DeniedDomains) == 0 {
t.Error("code template should have denied domains")
}
// Check command deny list
if len(cfg.Command.Deny) == 0 {
t.Error("code template should have denied commands")
}
}