From d8e55d95153288f2fbdaf43b00060f8c11ca4879 Mon Sep 17 00:00:00 2001 From: JY Tan Date: Sun, 28 Dec 2025 22:16:50 -0800 Subject: [PATCH] Introduce built-in templates for enhanced configuration options, support JSONC format --- cmd/fence/main.go | 86 +++++++--- docs/README.md | 2 +- docs/agents.md | 22 ++- docs/configuration.md | 2 +- docs/templates.md | 28 ++++ docs/templates/README.md | 19 --- docs/templates/agent-api-only.json | 8 - docs/templates/default-deny.json | 8 - docs/templates/npm-install.json | 8 - docs/templates/pip-install.json | 8 - docs/templates/workspace-write.json | 5 - go.mod | 1 + go.sum | 2 + internal/config/config.go | 9 +- internal/proxy/http_test.go | 35 ++++ internal/sandbox/macos.go | 3 + internal/templates/code.json | 157 ++++++++++++++++++ internal/templates/disable-telemetry.json | 116 +++++++++++++ .../templates/git-readonly.json | 0 .../templates/local-dev-server.json | 0 internal/templates/templates.go | 96 +++++++++++ internal/templates/templates_test.go | 123 ++++++++++++++ 22 files changed, 655 insertions(+), 83 deletions(-) create mode 100644 docs/templates.md delete mode 100644 docs/templates/README.md delete mode 100644 docs/templates/agent-api-only.json delete mode 100644 docs/templates/default-deny.json delete mode 100644 docs/templates/npm-install.json delete mode 100644 docs/templates/pip-install.json delete mode 100644 docs/templates/workspace-write.json create mode 100644 internal/templates/code.json create mode 100644 internal/templates/disable-telemetry.json rename {docs => internal}/templates/git-readonly.json (100%) rename {docs => internal}/templates/local-dev-server.json (100%) create mode 100644 internal/templates/templates.go create mode 100644 internal/templates/templates_test.go diff --git a/cmd/fence/main.go b/cmd/fence/main.go index 2b68a95..54f1106 100644 --- a/cmd/fence/main.go +++ b/cmd/fence/main.go @@ -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