basic sandboxing support with landrun, fail fast

This commit is contained in:
Levi Neely 2026-02-11 08:32:14 +01:00
parent 3c66c48241
commit 528a482fe8
12 changed files with 718 additions and 10 deletions

129
README.md
View File

@ -216,6 +216,135 @@ Backends create persistent tmux sessions named `anvillm-<backend>`:
These sessions persist across AnviLLM restarts and are cleaned up on exit.
## Sandboxing
AnviLLM uses [landrun](https://github.com/landlock-lsm/landrun) to sandbox backend processes for enhanced security. Sandboxing restricts filesystem and network access for LLM-controlled processes while allowing backends to run with full permissions (`--dangerously-skip-permissions`, `--trust-all-tools`) within the sandbox.
### How It Works
```
landrun --rw /project --rox /usr --connect-tcp 443 -- claude --dangerously-skip-permissions
↑ ↑
Sandbox layer (restricts access) Backend runs with full permissions
(but only within sandbox)
```
### Quick Start
Sandboxing is **enabled by default** with a locked-down policy:
- **Filesystem**: Read/write access to session working directory only
- **System binaries**: Read-only + execute access to `/usr`, `/lib`, `/bin`, `/sbin`
- **Network**: HTTPS (port 443) for API calls
- **Config files**: Access to `~/.claude`, `~/.kiro`, `~/.config/anvillm`
### Requirements
- **landrun**: Install with `go install github.com/landlock-lsm/landrun@latest`
- **Linux Kernel**: 5.13+ with Landlock support
- **Fallback**: Runs unsandboxed if landrun unavailable (best-effort mode)
### Configuration
Configure sandboxing via the `Sandbox` tag command in the main `/AnviLLM/` window:
1. Click **Sandbox** to open the configuration window
2. Click **Edit** to modify `~/.config/anvillm/sandbox.yaml`
3. Click **Reload** to reload configuration from disk
4. Create new sessions to apply changes
Configuration file: `~/.config/anvillm/sandbox.yaml`
**Important**: Changes apply to NEW sessions only. Active sessions are NOT affected.
### Path Templates
Use these templates in filesystem paths:
- `{CWD}` - Session working directory
- `{HOME}` - User home directory (`~`)
- `{TMPDIR}` - Temporary directory (`/tmp`)
### Common Configurations
#### Development (Permissive)
```yaml
network:
enabled: true
connect_tcp:
- "443"
- "80"
- "3000-9000"
filesystem:
rw:
- "{CWD}"
- "{HOME}/.npm"
- "{HOME}/.cache"
- "{HOME}/.local"
rox:
- "/usr"
- "/lib"
- "/bin"
```
#### Code Review (Restrictive)
```yaml
network:
enabled: false
filesystem:
ro:
- "{CWD}" # Read-only project access
rw:
- "{TMPDIR}"
rox:
- "/usr"
- "/bin"
```
#### Disabled (Testing Only)
```yaml
general:
enabled: false
```
### Security Model
**Default protections**:
- ✓ Cannot read sensitive files outside CWD (e.g., `~/.ssh/id_rsa`, `~/.aws/credentials`)
- ✓ Cannot modify system files
- ✓ Cannot make unauthorized network connections
- ✓ Cannot access files in other users' directories
**User can still**:
- Modify files in session CWD (by design - project workspace)
- Weaken sandboxing via config (user choice)
### Troubleshooting
**Landrun not available?**
- Install: `go install github.com/landlock-lsm/landrun@latest`
- Check status: Click `Sandbox` → `Status` in AnviLLM
**Network errors?**
- Ensure `connect_tcp = ["443"]` in config for API calls
- Or set `unrestricted = true` for full network access
**File access denied?**
- Add required paths to `rw` or `ro` in config
- Example for SSH keys: `ro = ["{HOME}/.ssh"]`
- Example for NPM: `rw = ["{HOME}/.npm", "{HOME}/.cache"]`
**Check sandbox status**:
```sh
# View current config
cat ~/.config/anvillm/sandbox.yaml
# Test landrun availability
landrun --version
```
For more details, see [landrun documentation](https://github.com/landlock-lsm/landrun).
## Troubleshooting
### "Backend not found" error

9
go.mod
View File

@ -1,5 +1,10 @@
module anvillm
go 1.21
go 1.21.0
require 9fans.net/go v0.0.7
toolchain go1.24.3
require (
9fans.net/go v0.0.7
gopkg.in/yaml.v3 v3.0.1
)

4
go.sum
View File

@ -32,3 +32,7 @@ golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -142,10 +142,21 @@ func (s *Session) waitForReady(ctx context.Context, timeout time.Duration) error
return fmt.Errorf("session closed")
}
s.output.Write(data)
output := s.output.String()
// Check for error patterns that indicate launch failure
if strings.Contains(output, "Error:") ||
strings.Contains(output, "ERROR") ||
strings.Contains(output, "Failed to apply sandbox") ||
strings.Contains(output, "missing kernel Landlock support") {
fmt.Fprintf(os.Stderr, "Error: backend failed to launch\n")
fmt.Fprintf(os.Stderr, "Backend output:\n%s\n", output)
return fmt.Errorf("backend launch failed")
}
// If we have a startup handler and it hasn't completed, call it
if !startupDone && s.startupHandler != nil {
keys, done := s.startupHandler.HandleStartup(s.output.String())
keys, done := s.startupHandler.HandleStartup(output)
if keys != "" {
debug.Log("[session %s] startup handler sending keys: %q", s.id, keys)
if err := sendKeys(s.target(), keys); err != nil {
@ -160,13 +171,20 @@ func (s *Session) waitForReady(ctx context.Context, timeout time.Duration) error
}
// Check if ready (only after startup is done)
if startupDone && s.detector.IsReady(s.output.String()) {
if startupDone && s.detector.IsReady(output) {
debug.Log("[session %s] ready detected", s.id)
return nil
}
case <-time.After(time.Until(deadline)):
if time.Now().After(deadline) {
debug.Log("[session %s] timeout, current output: %s", s.id, s.output.String())
output := s.output.String()
debug.Log("[session %s] timeout, current output: %s", s.id, output)
if output != "" {
fmt.Fprintf(os.Stderr, "Error: timeout waiting for backend to be ready\n")
fmt.Fprintf(os.Stderr, "Backend output:\n%s\n", output)
} else {
fmt.Fprintf(os.Stderr, "Error: timeout waiting for backend to be ready (no output received)\n")
}
return fmt.Errorf("timeout waiting for ready")
}
}

View File

@ -4,6 +4,7 @@ package tmux
import (
"anvillm/internal/backend"
"anvillm/internal/debug"
"anvillm/internal/sandbox"
"context"
"crypto/rand"
"fmt"
@ -78,6 +79,7 @@ func New(cfg Config) backend.Backend {
if cfg.TmuxSize.Cols == 0 {
cfg.TmuxSize.Cols = 120
}
return &Backend{
cfg: cfg,
tmuxSession: fmt.Sprintf("anvillm-%s", cfg.Name),
@ -115,6 +117,14 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
debug.Log("[session %s] creating window in tmux session %s", id, b.tmuxSession)
// Load sandbox configuration fresh for each session
sandboxCfg, err := sandbox.Load()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading sandbox config: %v (using defaults)\n", err)
debug.Log("[session %s] failed to load sandbox config: %v, using defaults", id, err)
sandboxCfg = sandbox.DefaultConfig()
}
// 1. Ensure persistent tmux session exists
if err := b.ensureTmuxSession(); err != nil {
return nil, fmt.Errorf("failed to ensure tmux session: %w", err)
@ -164,9 +174,29 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
fifoOpenCh <- f
}()
// 7. Start the command in tmux window
// 7. Wrap command with landrun if sandboxing enabled
command := b.cfg.Command
if sandboxCfg.General.Enabled {
if !sandbox.IsAvailable() {
if sandboxCfg.General.BestEffort {
fmt.Fprintf(os.Stderr, "Warning: landrun not available, running unsandboxed (best-effort mode)\n")
} else {
os.Remove(fifoPath)
killWindow(b.tmuxSession, windowName)
fmt.Fprintf(os.Stderr, "Error: sandboxing enabled but landrun not available\n")
fmt.Fprintf(os.Stderr, "Install landrun or enable best_effort mode in ~/.config/anvillm/sandbox.yaml\n")
return nil, fmt.Errorf("sandboxing enabled but landrun not available")
}
}
command = sandbox.WrapCommand(sandboxCfg, command, cwd)
debug.Log("[session %s] sandboxed command: %v", id, command)
} else {
debug.Log("[session %s] unsandboxed command: %v", id, command)
}
// 8. Build command string for tmux
cmdStr := ""
for i, arg := range b.cfg.Command {
for i, arg := range command {
if i > 0 {
cmdStr += " "
}
@ -179,7 +209,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
}
// Change to working directory first
if err := sendKeys(target, "cd", fmt.Sprintf("\"%s\"", cwd), "C-m"); err != nil {
if err := sendKeys(target, fmt.Sprintf("cd \"%s\"", cwd), "C-m"); err != nil {
os.Remove(fifoPath)
killWindow(b.tmuxSession, windowName)
return nil, fmt.Errorf("failed to change directory: %w", err)

View File

@ -18,7 +18,7 @@ import (
// NewClaude creates a claude CLI backend
//
// Uses tmux backend to handle interactive TUI dialogs programmatically.
// The startup handler automatically accepts the --dangerously-skip-permissions dialog.
// Runs with --dangerously-skip-permissions because landrun provides the actual sandboxing.
//
// Sessions are automatically saved by Claude to ~/.claude/projects/<dir-path>/<session-id>.jsonl
func NewClaude() backend.Backend {

View File

@ -15,6 +15,7 @@ import (
)
// NewKiroCLI creates a kiro-cli backend
// Runs with --trust-all-tools because landrun provides the actual sandboxing
func NewKiroCLI() backend.Backend {
return tmux.New(tmux.Config{
Name: "kiro-cli",

171
internal/sandbox/config.go Normal file
View File

@ -0,0 +1,171 @@
package sandbox
import (
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// Config represents the global sandbox configuration
type Config struct {
General GeneralConfig `yaml:"general"`
Filesystem FilesystemConfig `yaml:"filesystem"`
Network NetworkConfig `yaml:"network"`
Advanced AdvancedConfig `yaml:"advanced"`
}
// GeneralConfig contains general sandbox settings
type GeneralConfig struct {
Enabled bool `yaml:"enabled"`
BestEffort bool `yaml:"best_effort"`
LogLevel string `yaml:"log_level"`
}
// FilesystemConfig contains filesystem permission settings
type FilesystemConfig struct {
RO []string `yaml:"ro"` // Read-only
ROX []string `yaml:"rox"` // Read-only with execute
RW []string `yaml:"rw"` // Read-write
RWX []string `yaml:"rwx"` // Read-write with execute
}
// NetworkConfig contains network permission settings
type NetworkConfig struct {
Enabled bool `yaml:"enabled"`
Unrestricted bool `yaml:"unrestricted"`
BindTCP []string `yaml:"bind_tcp"`
ConnectTCP []string `yaml:"connect_tcp"`
}
// AdvancedConfig contains advanced landrun settings
type AdvancedConfig struct {
LDD bool `yaml:"ldd"`
AddExec bool `yaml:"add_exec"`
}
// DefaultConfig returns the default locked-down configuration
func DefaultConfig() *Config {
return &Config{
General: GeneralConfig{
Enabled: true,
BestEffort: false, // Fail-closed: refuse to run if sandboxing unavailable
LogLevel: "error",
},
Filesystem: FilesystemConfig{
RO: []string{
"/etc/passwd", // UID→homedir lookup (getpwuid)
"/dev/null", // Null device
"/proc/meminfo", // Memory info
"/proc/self/cgroup", // Cgroup info
"/proc/self/maps", // Process memory maps
"/proc/version", // Kernel version
},
ROX: []string{
"/usr",
"/lib",
"/lib64",
"/bin",
"/sbin",
},
RW: []string{
"{CWD}",
"{TMPDIR}",
"{HOME}/.claude",
"{HOME}/.kiro",
"{HOME}/.config/anvillm",
"{HOME}/.claude.json",
},
RWX: []string{},
},
Network: NetworkConfig{
Enabled: false,
Unrestricted: true, // Unrestricted network (fine-grained restrictions require Landlock v5+)
BindTCP: []string{},
ConnectTCP: []string{"443"}, // Example: HTTPS for API calls (when Enabled: true)
},
Advanced: AdvancedConfig{
LDD: false, // Disabled: fails on Node.js scripts like claude
AddExec: true,
},
}
}
// ConfigPath returns the path to the config file
func ConfigPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "anvillm", "sandbox.yaml")
}
// Load loads the config from disk, creating defaults if it doesn't exist
func Load() (*Config, error) {
path := ConfigPath()
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
// Create default config
cfg := DefaultConfig()
if saveErr := Save(cfg); saveErr != nil {
// Return defaults even if save fails
return cfg, saveErr
}
return cfg, nil
}
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// Save saves the config to disk with explanatory comments
func Save(cfg *Config) error {
path := ConfigPath()
// Ensure directory exists
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
// Prepend header with documentation
header := `# AnviLLM Sandbox Configuration
# This file controls landrun sandboxing for all backend sessions
# Documentation: https://github.com/landlock-lsm/landrun
#
# Path templates:
# {CWD} - Session working directory
# {HOME} - User home directory
# {TMPDIR} - Temporary directory (/tmp or $TMPDIR)
#
# WARNING: CLI tools need network access to function!
# - Claude requires connect_tcp for claude.com
# - Kiro requires connect_tcp for kiro.ai
#
# Changes apply to NEW sessions only. Existing sessions are not affected.
`
data = append([]byte(header), data...)
return os.WriteFile(path, data, 0644)
}
// expandPath replaces template variables with actual values
func expandPath(pattern, cwd, home, tmpdir string) string {
s := pattern
s = strings.ReplaceAll(s, "{CWD}", cwd)
s = strings.ReplaceAll(s, "{HOME}", home)
s = strings.ReplaceAll(s, "{TMPDIR}", tmpdir)
return s
}

View File

@ -0,0 +1,37 @@
package sandbox
import (
"os/exec"
"strings"
"sync"
)
var (
available bool
availableOnce sync.Once
)
// IsAvailable checks if landrun is available on the system.
// The result is cached after the first call for performance.
func IsAvailable() bool {
availableOnce.Do(func() {
_, err := exec.LookPath("landrun")
available = (err == nil)
})
return available
}
// Version returns the landrun version string, or empty if unavailable.
func Version() string {
if !IsAvailable() {
return ""
}
cmd := exec.Command("landrun", "--version")
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}

158
internal/sandbox/wrapper.go Normal file
View File

@ -0,0 +1,158 @@
package sandbox
import (
"fmt"
"os"
"strings"
)
// WrapCommand wraps a command with landrun based on the configuration.
// Returns the wrapped command args, or the original command if sandboxing is disabled.
func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string {
if !cfg.General.Enabled {
return originalCmd
}
if !IsAvailable() {
if cfg.General.BestEffort {
// Silently fall back to unsandboxed execution
return originalCmd
}
// Strict mode: would fail, but caller should check IsAvailable() first
return originalCmd
}
home, _ := os.UserHomeDir()
tmpdir := os.Getenv("TMPDIR")
if tmpdir == "" {
tmpdir = "/tmp"
}
args := []string{"landrun"}
// Log level
if cfg.General.LogLevel != "" {
args = append(args, "--log-level", cfg.General.LogLevel)
}
// Best effort mode
if cfg.General.BestEffort {
args = append(args, "--best-effort")
}
// Advanced options
if cfg.Advanced.LDD {
args = append(args, "--ldd")
}
if cfg.Advanced.AddExec {
args = append(args, "--add-exec")
}
// Filesystem permissions - RO (read-only, no execute)
for _, path := range cfg.Filesystem.RO {
expanded := expandPath(path, cwd, home, tmpdir)
if pathExists(expanded) {
args = append(args, "--ro", expanded)
}
}
// Filesystem permissions - ROX (read-only with execute)
for _, path := range cfg.Filesystem.ROX {
expanded := expandPath(path, cwd, home, tmpdir)
if pathExists(expanded) {
args = append(args, "--rox", expanded)
}
}
// Filesystem permissions - RW (read-write, no execute)
for _, path := range cfg.Filesystem.RW {
expanded := expandPath(path, cwd, home, tmpdir)
if pathExists(expanded) {
args = append(args, "--rw", expanded)
}
}
// Filesystem permissions - RWX (read-write with execute)
for _, path := range cfg.Filesystem.RWX {
expanded := expandPath(path, cwd, home, tmpdir)
if pathExists(expanded) {
args = append(args, "--rwx", expanded)
}
}
// Network permissions
if cfg.Network.Unrestricted {
args = append(args, "--unrestricted-network")
} else if cfg.Network.Enabled {
for _, port := range cfg.Network.BindTCP {
args = append(args, "--bind-tcp", port)
}
for _, port := range cfg.Network.ConnectTCP {
args = append(args, "--connect-tcp", port)
}
}
// If network not enabled and not unrestricted, no network flags = no network access
// Append original command after --
args = append(args, "--")
args = append(args, originalCmd...)
return args
}
// BuildSummary creates a human-readable summary of sandbox settings
func BuildSummary(cfg *Config) string {
var lines []string
if !cfg.General.Enabled {
return "Sandboxing: DISABLED"
}
lines = append(lines, "Sandboxing: ENABLED")
if cfg.General.BestEffort {
lines = append(lines, " Mode: Best-effort (fallback if landrun unavailable)")
} else {
lines = append(lines, " Mode: Strict (require landrun)")
}
// Filesystem
lines = append(lines, "")
lines = append(lines, "Filesystem:")
if len(cfg.Filesystem.RW) > 0 {
lines = append(lines, fmt.Sprintf(" Read-Write: %s", strings.Join(cfg.Filesystem.RW, ", ")))
}
if len(cfg.Filesystem.RO) > 0 {
lines = append(lines, fmt.Sprintf(" Read-Only: %s", strings.Join(cfg.Filesystem.RO, ", ")))
}
if len(cfg.Filesystem.ROX) > 0 {
lines = append(lines, fmt.Sprintf(" Read-Execute: %s", strings.Join(cfg.Filesystem.ROX, ", ")))
}
if len(cfg.Filesystem.RWX) > 0 {
lines = append(lines, fmt.Sprintf(" Read-Write-Execute: %s", strings.Join(cfg.Filesystem.RWX, ", ")))
}
// Network
lines = append(lines, "")
if cfg.Network.Unrestricted {
lines = append(lines, "Network: UNRESTRICTED")
} else if cfg.Network.Enabled {
lines = append(lines, "Network: LIMITED")
if len(cfg.Network.ConnectTCP) > 0 {
lines = append(lines, fmt.Sprintf(" Connect TCP: %s", strings.Join(cfg.Network.ConnectTCP, ", ")))
}
if len(cfg.Network.BindTCP) > 0 {
lines = append(lines, fmt.Sprintf(" Bind TCP: %s", strings.Join(cfg.Network.BindTCP, ", ")))
}
} else {
lines = append(lines, "Network: BLOCKED")
}
return strings.Join(lines, "\n")
}
// pathExists checks if a file or directory exists
func pathExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

150
internal/ui/sandbox.go Normal file
View File

@ -0,0 +1,150 @@
package ui
import (
"anvillm/internal/sandbox"
"fmt"
"os"
"os/exec"
"strings"
"9fans.net/go/acme"
)
// OpenSandboxWindow opens the sandbox configuration window
func OpenSandboxWindow() error {
w, err := acme.New()
if err != nil {
return err
}
w.Name("/AnviLLM/Sandbox")
w.Write("tag", []byte("Get Edit Reload Status "))
go handleSandboxWindow(w)
return nil
}
func handleSandboxWindow(w *acme.Win) {
defer w.CloseFiles()
// Load and display current config
refreshSandboxWindow(w)
for e := range w.EventChan() {
switch e.C2 {
case 'x', 'X':
cmd := string(e.Text)
switch cmd {
case "Get":
refreshSandboxWindow(w)
case "Edit":
openConfigInEditor()
case "Reload":
// Config is edited externally, just reload
refreshSandboxWindow(w)
fmt.Println("Reloaded config from disk. Changes apply to new sessions only.")
case "Status":
showStatus(w)
default:
w.WriteEvent(e)
}
default:
w.WriteEvent(e)
}
}
}
func refreshSandboxWindow(w *acme.Win) {
cfg, err := sandbox.Load()
if err != nil {
w.Addr(",")
w.Write("data", []byte(fmt.Sprintf("Error loading config: %v\n", err)))
w.Ctl("clean")
return
}
var buf strings.Builder
buf.WriteString("AnviLLM Sandbox Configuration\n")
buf.WriteString(strings.Repeat("=", 60) + "\n\n")
// Summary
buf.WriteString(sandbox.BuildSummary(cfg))
buf.WriteString("\n\n")
// Instructions
buf.WriteString(strings.Repeat("-", 60) + "\n")
buf.WriteString("Commands:\n")
buf.WriteString(" Edit - Open config file in $EDITOR or Acme\n")
buf.WriteString(" Reload - Reload config from disk\n")
buf.WriteString(" Status - Check landrun availability\n")
buf.WriteString(" Get - Refresh this window\n\n")
buf.WriteString("Config file: " + sandbox.ConfigPath() + "\n")
buf.WriteString("Note: Changes apply to NEW sessions only.\n")
buf.WriteString(" Active sessions are NOT affected.\n")
w.Addr(",")
w.Write("data", []byte(buf.String()))
w.Ctl("clean")
}
func openConfigInEditor() {
path := sandbox.ConfigPath()
// Try to open in Acme via 9p
cmd := exec.Command("9p", "write", "acme/new/ctl")
stdin, err := cmd.StdinPipe()
if err == nil {
cmd.Start()
fmt.Fprintf(stdin, "name %s\n", path)
fmt.Fprintf(stdin, "get\n")
stdin.Close()
cmd.Wait()
return
}
// Fallback: use environment EDITOR
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
cmd = exec.Command(editor, path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Run()
}
func showStatus(w *acme.Win) {
var buf strings.Builder
buf.WriteString("\n")
buf.WriteString(strings.Repeat("-", 60) + "\n")
buf.WriteString("Landrun Status:\n")
buf.WriteString(strings.Repeat("-", 60) + "\n")
if sandbox.IsAvailable() {
buf.WriteString("Status: AVAILABLE\n")
version := sandbox.Version()
if version != "" {
buf.WriteString("Version: " + version + "\n")
}
if path, err := exec.LookPath("landrun"); err == nil {
buf.WriteString("Location: " + path + "\n")
}
} else {
buf.WriteString("Status: NOT AVAILABLE\n")
buf.WriteString("\n")
buf.WriteString("Install landrun:\n")
buf.WriteString(" go install github.com/landlock-lsm/landrun@latest\n")
buf.WriteString("\n")
buf.WriteString("Or download from:\n")
buf.WriteString(" https://github.com/landlock-lsm/landrun\n")
}
w.Addr("$")
w.Write("data", []byte(buf.String()))
}

View File

@ -8,6 +8,7 @@ import (
"anvillm/internal/debug"
"anvillm/internal/p9"
"anvillm/internal/session"
"anvillm/internal/ui"
"context"
"flag"
"fmt"
@ -85,7 +86,7 @@ func main() {
defer w.CloseFiles()
w.Name(windowName)
w.Write("tag", []byte("Kiro Claude Open Kill Get Login "))
w.Write("tag", []byte("Kiro Claude Open Kill Get Sandbox Login "))
refreshList(w, mgr)
w.Ctl("clean")
@ -210,6 +211,10 @@ func main() {
state = "enabled"
}
fmt.Printf("Debug mode %s\n", state)
case "Sandbox":
if err := ui.OpenSandboxWindow(); err != nil {
fmt.Fprintf(os.Stderr, "Error opening sandbox window: %v\n", err)
}
case "Login":
cmd := exec.Command("firefox")
cmd.Start()