execute_code: configurable sandbox, fix dangerous patterns

- Add sandbox parameter to execute_code (default: anvilmcp)
- Load sandbox config from ~/.config/anvillm/sandbox/<name>.yaml
- Fallback to ./cfg/sandbox/<name>.yaml for development
- Add cwd config option: 'temp' for isolated workspace, '{CWD}' for caller's dir
- Create cfg/sandbox/anvilmcp.yaml with restricted defaults
- Update cfg/sandbox/default.yaml with cwd: '{CWD}'

Dangerous pattern fixes (bash, not JS/Python):
- Remove curl/wget block (network controlled by landrun)
- Remove chmod 777, eval(), exec() (wrong syntax)
- Allow /etc/passwd (world-readable, needed for user lookups)
- Fix /dev/ write check to only block /dev/sd* (not /dev/null)
- Fix eval pattern for actual bash usage

Update skills with explicit sandbox specifications:
- anvilmcp sandbox for normal operations
- default sandbox for spawn_agent.sh (needs full permissions)
This commit is contained in:
Levi Neely 2026-03-06 09:53:41 +01:00
parent b51bbf7e72
commit 0d847f96f3
11 changed files with 405 additions and 140 deletions

36
cfg/sandbox/anvilmcp.yaml Normal file
View File

@ -0,0 +1,36 @@
# Default sandbox config for anvilmcp execute_code
# Based on hard-coded permissions from execute.go
cwd: temp # Use temp workspace (cleaned up after execution)
filesystem:
ro:
- "/etc/alternatives"
- "/etc/ld.so.cache"
- "/etc/passwd"
- "/dev/urandom"
rox:
- "/usr"
- "/lib"
- "/lib64"
- "/bin"
- "{HOME}/go/bin"
- "{HOME}/bin"
- "{HOME}/.local/bin"
- "{HOME}/opt"
- "{PLAN9}/bin"
rw:
- "/dev/null"
- "/tmp"
- "{NAMESPACE}"
rwx:
- "{CWD}"
env:
- NAMESPACE
- PATH
- HOME
- PLAN9
- DISPLAY
- WAYLAND_DISPLAY
- USER
- AGENT_ID
network:
unrestricted: true

View File

@ -1,5 +1,6 @@
# Default role: combined most-permissive settings from all tasks/roles
# TODO: revisit role-based permissions later
cwd: "{CWD}"
filesystem:
ro:
- "{HOME}/.gitconfig"

Binary file not shown.

View File

@ -12,6 +12,8 @@ import (
"strings"
"sync"
"time"
"gopkg.in/yaml.v3"
)
var dangerousPatterns = []*regexp.Regexp{
@ -22,16 +24,13 @@ var dangerousPatterns = []*regexp.Regexp{
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+\.\.?(/|$)`), // rm -rf ./ or rm -rf ../
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+~`), // rm -rf ~ (home dir)
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+\*`), // rm -rf * (glob expansion)
regexp.MustCompile(`:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&`), // fork bomb
regexp.MustCompile(`\bmkfs\b`), // filesystem format
regexp.MustCompile(`\bdd\b.*\bif=/dev/`), // dd from device
regexp.MustCompile(`\bchmod\s+777\b`), // world-writable
regexp.MustCompile(`\b(curl|wget)\s+\S*://`), // network fetch (any protocol)
regexp.MustCompile(`>\s*/dev/`), // write to device
regexp.MustCompile(`\beval\s*\(`), // eval execution
regexp.MustCompile(`\bexec\s*\(`), // exec execution
regexp.MustCompile(`\b(sudo|su)\b`), // privilege escalation
regexp.MustCompile(`/etc/(passwd|shadow|sudoers)`), // sensitive files
regexp.MustCompile(`:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&`), // fork bomb
regexp.MustCompile(`\bmkfs\b`), // filesystem format
regexp.MustCompile(`\bdd\b.*\bif=/dev/`), // dd from device
regexp.MustCompile(`>\s*/dev/sd`), // write to block device
regexp.MustCompile(`\beval\s+".*\$`), // eval with variable expansion
regexp.MustCompile(`\b(sudo|su)\s`), // privilege escalation
regexp.MustCompile(`/etc/(shadow|sudoers)`), // sensitive files (not passwd)
}
var whitespacePattern = regexp.MustCompile(`\s+`)
@ -109,7 +108,95 @@ func validateCode(code string) error {
return nil
}
func executeCode(code, language string, timeout int) (string, error) {
// SandboxConfig represents the sandbox configuration loaded from YAML
type SandboxConfig struct {
CWD string `yaml:"cwd"` // "temp" for temp workspace, or path template like "{CWD}"
Filesystem struct {
RO []string `yaml:"ro"`
ROX []string `yaml:"rox"`
RW []string `yaml:"rw"`
RWX []string `yaml:"rwx"`
} `yaml:"filesystem"`
Env []string `yaml:"env"`
Network struct {
Unrestricted bool `yaml:"unrestricted"`
} `yaml:"network"`
}
// loadSandboxConfig loads sandbox config from ~/.config/anvillm/sandbox/<name>.yaml
// Falls back to ./cfg/sandbox/<name>.yaml for development
func loadSandboxConfig(name string) (*SandboxConfig, error) {
// Validate name to prevent path traversal
if name == "" {
name = "anvilmcp"
}
for _, r := range name {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_') {
return nil, fmt.Errorf("invalid sandbox name: %s", name)
}
}
homeDir, _ := os.UserHomeDir()
cfgPath := filepath.Join(homeDir, ".config", "anvillm", "sandbox", name+".yaml")
// Fall back to ./cfg/sandbox/ for development
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
cfgPath = filepath.Join("cfg", "sandbox", name+".yaml")
}
data, err := os.ReadFile(cfgPath)
if err != nil {
return nil, fmt.Errorf("failed to load sandbox config %s: %v", name, err)
}
var cfg SandboxConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse sandbox config: %v", err)
}
return &cfg, nil
}
// expandPath replaces template variables with actual values
func expandPath(pattern string) string {
s := pattern
homeDir, _ := os.UserHomeDir()
// Generic environment variable expansion: {VAR_NAME}
re := regexp.MustCompile(`\{([A-Z_][A-Z0-9_]*)\}`)
s = re.ReplaceAllStringFunc(s, func(match string) string {
varName := match[1 : len(match)-1]
switch varName {
case "HOME":
return homeDir
case "PLAN9":
if plan9 := os.Getenv("PLAN9"); plan9 != "" {
return plan9
}
return "" // Will cause path to be skipped
case "NAMESPACE":
if ns := os.Getenv("NAMESPACE"); ns != "" {
return ns
}
if nsCmd, err := exec.Command("namespace").Output(); err == nil {
return strings.TrimSpace(string(nsCmd))
}
return fmt.Sprintf("/tmp/ns.%s.:0", os.Getenv("USER"))
case "CWD":
return "" // Handled specially - replaced with workDir
}
if val := os.Getenv(varName); val != "" {
return val
}
return "" // Will cause path to be skipped
})
return s
}
func executeCode(code, language string, timeout int, sandbox string) (string, error) {
start := time.Now()
if timeout <= 0 {
@ -129,113 +216,92 @@ func executeCode(code, language string, timeout int) (string, error) {
return "", err
}
// Load sandbox config
cfg, err := loadSandboxConfig(sandbox)
if err != nil {
return "", err
}
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home dir: %v", err)
}
// Determine working directory based on config
var workDir string
var cleanupWorkDir bool
workspaceBase := filepath.Join(homeDir, ".cache", "anvillm", "exec")
if err := os.MkdirAll(workspaceBase, 0700); err != nil {
return "", fmt.Errorf("failed to create workspace base: %v", err)
if cfg.CWD == "" || cfg.CWD == "temp" {
// Use temp workspace (original behavior)
workspaceBase := filepath.Join(homeDir, ".cache", "anvillm", "exec")
if err := os.MkdirAll(workspaceBase, 0700); err != nil {
return "", fmt.Errorf("failed to create workspace base: %v", err)
}
workDir, err = os.MkdirTemp(workspaceBase, "anvilmcp-*")
if err != nil {
return "", fmt.Errorf("failed to create workspace: %v", err)
}
cleanupWorkDir = true
} else {
// Use configured path (e.g., "{CWD}" expands to current directory)
workDir = expandPath(cfg.CWD)
if workDir == "" {
workDir, _ = os.Getwd()
}
cleanupWorkDir = false
}
workDir, err := os.MkdirTemp(workspaceBase, "anvilmcp-*")
if err != nil {
return "", fmt.Errorf("failed to create workspace: %v", err)
if cleanupWorkDir {
defer os.RemoveAll(workDir)
}
defer os.RemoveAll(workDir)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
// Check for landrun
landrunPath, err := exec.LookPath("landrun")
if err != nil {
return "", fmt.Errorf("landrun not found: %v", err)
}
// Get namespace for 9p access
namespace := os.Getenv("NAMESPACE")
if namespace == "" {
// Try to get from namespace command
if nsCmd, err := exec.Command("namespace").Output(); err == nil {
namespace = strings.TrimSpace(string(nsCmd))
}
if namespace == "" {
namespace = fmt.Sprintf("/tmp/ns.%s.:0", os.Getenv("USER"))
}
}
// Get AGENT_ID if set
agentID := os.Getenv("AGENT_ID")
var cmd *exec.Cmd
switch language {
case "bash", "":
args := []string{
"--rwx", workDir,
"--unrestricted-network",
}
// Add common system paths if they exist
systemPaths := []struct {
flag string
path string
}{
{"--rox", "/usr"},
{"--rox", "/lib"},
{"--rox", "/lib64"},
{"--rox", "/bin"},
{"--ro", "/etc/alternatives"},
{"--ro", "/etc/ld.so.cache"},
{"--ro", "/etc/passwd"},
{"--rw", "/dev/null"},
{"--ro", "/dev/urandom"},
{"--rw", "/tmp"},
}
for _, sp := range systemPaths {
if _, err := os.Stat(sp.path); err == nil {
args = append(args, sp.flag, sp.path)
args := []string{}
// Add filesystem permissions from config
addPaths := func(flag string, paths []string) {
for _, p := range paths {
expanded := expandPath(p)
if expanded == "" {
continue
}
// Handle {CWD} specially
if strings.Contains(p, "{CWD}") {
expanded = strings.ReplaceAll(p, "{CWD}", workDir)
}
if _, err := os.Stat(expanded); err == nil {
args = append(args, flag, expanded)
}
}
}
// Add namespace if set
if namespace != "" {
args = append(args, "--rw", namespace)
addPaths("--ro", cfg.Filesystem.RO)
addPaths("--rox", cfg.Filesystem.ROX)
addPaths("--rw", cfg.Filesystem.RW)
addPaths("--rwx", cfg.Filesystem.RWX)
// Network
if cfg.Network.Unrestricted {
args = append(args, "--unrestricted-network")
}
// Add common user binary directories
userBinDirs := []string{
filepath.Join(homeDir, "bin"),
filepath.Join(homeDir, ".local", "bin"),
filepath.Join(homeDir, "opt"),
filepath.Join(homeDir, "go", "bin"),
}
// Add PLAN9 bin if set
if plan9 := os.Getenv("PLAN9"); plan9 != "" {
userBinDirs = append(userBinDirs, filepath.Join(plan9, "bin"))
}
for _, dir := range userBinDirs {
if _, err := os.Stat(dir); err == nil {
args = append(args, "--rox", dir)
// Environment variables
for _, env := range cfg.Env {
if os.Getenv(env) != "" || env == "HOME" || env == "USER" || env == "PATH" {
args = append(args, "--env", env)
}
}
args = append(args,
"--env", "NAMESPACE",
"--env", "PATH",
"--env", "HOME",
"--env", "PLAN9",
"--env", "DISPLAY",
"--env", "WAYLAND_DISPLAY",
"--env", "USER",
)
if agentID != "" {
args = append(args, "--env", "AGENT_ID")
}
args = append(args, "--", "bash", "-c", code)
cmd = exec.CommandContext(ctx, landrunPath, args...)
@ -253,7 +319,6 @@ func executeCode(code, language string, timeout int) (string, error) {
err = cmd.Run()
output := outputBuf.Bytes()
// Append truncation notice if output was truncated
if lw.truncated {
output = append(output, []byte("\n[output truncated at 10MB]")...)
}

View File

@ -95,6 +95,7 @@ func main() {
"code": {Type: "string", Description: "Bash code to execute"},
"language": {Type: "string", Description: "Programming language (bash)", Enum: []string{"bash"}},
"timeout": {Type: "integer", Description: "Timeout in seconds (default: 30)"},
"sandbox": {Type: "string", Description: "Sandbox config name (default: default)"},
},
Required: []string{"code"},
},
@ -132,13 +133,17 @@ func handleToolCall(req MCPRequest) {
if t, ok := params.Arguments["timeout"].(float64); ok {
timeout = int(t)
}
sandbox, _ := params.Arguments["sandbox"].(string)
if sandbox == "" {
sandbox = "anvilmcp"
}
// Acquire execution slot
executionSemaphore <- struct{}{}
defer func() { <-executionSemaphore }()
fmt.Fprintf(os.Stderr, "[anvilmcp] Executing bash code (timeout: %ds)\n", timeout)
result, err := executeCode(code, language, timeout)
fmt.Fprintf(os.Stderr, "[anvilmcp] Executing bash code (timeout: %ds, sandbox: %s)\n", timeout, sandbox)
result, err := executeCode(code, language, timeout, sandbox)
// Log token comparison
codeTokens := estimateTokens(code)

View File

@ -9,12 +9,28 @@ when_to_load: Load when you need to send messages to other agents, respond to th
## Tools
**IMPORTANT:** All anvillm-communication tools must be run via `execute_code` tool.
**IMPORTANT:** Read the $AGENT_ID environment variable to get your agent ID
- Discover agents: `bash <(9p read agent/tools/agents/list_sessions.sh) | grep "$(pwd)"`
- Send message: `bash <(9p read agent/tools/messaging/send_message.sh) <FROM> <TO> <TYPE> <SUBJECT> <BODY>`
- Check inbox: `bash <(9p read agent/tools/messaging/read_inbox.sh) <AGENT_ID>`
Discover agents:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/agents/list_sessions.sh) | grep "$(pwd)"
```
Send message:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/messaging/send_message.sh) <FROM> <TO> <TYPE> <SUBJECT> <BODY>
```
Check inbox:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/messaging/read_inbox.sh) <AGENT_ID>
```
## Rules

View File

@ -10,13 +10,42 @@ Create and control agent sessions.
## Commands
All commands must be run via `execute_code` tool, EXCEPT spawn_agent.sh which must NOT use execute_code:
All commands use `execute_code` with `sandbox: "anvilmcp"`, EXCEPT spawn_agent.sh which requires `sandbox: "default"`.
- List sessions: `list_sessions.sh`
- Create session: `create_session.sh <backend> <cwd> [sandbox=<sandbox>] [model=<model>]`
- Control session: `control_session.sh <session-id> <command>`
- Spawn agent: `spawn_agent.sh <agent-id> [cwd-path] [initial-context-prompt]` (do NOT use execute_code)
- Kill agent: `kill_agent.sh [cwd-path]`
List sessions:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/agents/list_sessions.sh)
```
Create session:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/agents/create_session.sh) <backend> <cwd> [sandbox=<sandbox>] [model=<model>]
```
Control session:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/agents/control_session.sh) <session-id> <command>
```
Spawn agent:
```
Tool: execute_code
sandbox: default
code: bash <(9p read agent/tools/agents/spawn_agent.sh) <agent-id> [cwd-path] [initial-context-prompt]
```
Kill agent:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/agents/kill_agent.sh) [cwd-path]
```
## Backends

View File

@ -8,33 +8,123 @@ description: Manage tasks using the beads 9P interface. Use when creating, updat
## Project Mount Discovery
1. Find your project's beads mount: `MOUNT=$(bash <(9p read agent/tools/beads/list_mounts.sh) | grep "$(pwd)" | awk '{print $1}')`
2. If no mount exists, create one and use it: `bash <(9p read agent/tools/beads/mount_beads.sh) "$(pwd)"`. The mount will be named for the base directory of $(pwd).
Find your project's beads mount:
```
Tool: execute_code
sandbox: anvilmcp
code: MOUNT=$(bash <(9p read agent/tools/beads/list_mounts.sh) | grep "$(pwd)" | awk '{print $1}'); echo $MOUNT
```
If no mount exists, create one:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/mount_beads.sh) "$(pwd)"
```
## Tools
**IMPORTANT:** All beads tools listed must be run via `execute_code` tool.
All tools require `<mount>` as first argument. Exceptions: `list_mounts.sh`, `mount_beads.sh`, `umount_beads.sh`.
**IMPORTANT:** All tools require `<mount>` as first argument. Exceptions: `list_mounts.sh`, `mount_beads.sh`, `umount_beads.sh`.
List mounts:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/list_mounts.sh)
```
- List mounts: `bash <(9p read tools/beads/list_mounts.sh)`
- List ready beads: `bash <(9p read tools/beads/list_ready_beads.sh) <mount>`
- List all beads: `bash <(9p read tools/beads/list_beads.sh) <mount>`
- Read bead: `bash <(9p read tools/beads/read_bead.sh) <mount> <id> <property>`
- Read bead JSON: `bash <(9p read tools/beads/read_bead.sh) <mount> <id> json`
- Read bead comments: `bash <(9p read tools/beads/read_bead.sh) <mount> <id> comments`
List ready beads:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/list_ready_beads.sh) <mount>
```
List all beads:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/list_beads.sh) <mount>
```
Read bead JSON:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/read_bead.sh) <mount> <id> json
```
Read bead comments:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/read_bead.sh) <mount> <id> comments
```
**IMPORTANT:** When working on a bead, always check `comment_count` in the JSON output. If > 0, read comments for additional context, decisions, or blockers.
- Claim bead: `bash <(9p read tools/beads/claim_bead.sh) <mount> bd-abc [$AGENT_ID]`
- Complete bead: `bash <(9p read tools/beads/complete_bead.sh) <mount> bd-abc`
- Fail bead: `bash <(9p read tools/beads/fail_bead.sh) <mount> bd-abc "reason"`
- Create bead: `bash <(9p read tools/beads/create_bead.sh) <mount> "title" "desc" [parent]`
- Update bead: `bash <(9p read tools/beads/update_bead.sh) <mount> bd-abc <field> <value>`
- Add dependency: `bash <(9p read tools/beads/add_dependency.sh) <mount> <child> <parent>`
- Comment on bead: `bash <(9p read tools/beads/comment_bead.sh) <mount> bd-abc "text"`
- Label bead: `bash <(9p read tools/beads/label_bead.sh) <mount> bd-abc <label>`
- Delete bead: `bash <(9p read tools/beads/delete_bead.sh) <mount> bd-abc`
Claim bead:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/claim_bead.sh) <mount> <id> [$AGENT_ID]
```
Complete bead:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/complete_bead.sh) <mount> <id>
```
Fail bead:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/fail_bead.sh) <mount> <id> "reason"
```
Create bead:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/create_bead.sh) <mount> "title" "desc" [parent]
```
Update bead:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/update_bead.sh) <mount> <id> <field> <value>
```
Add dependency:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/add_dependency.sh) <mount> <child> <parent>
```
Comment on bead:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/comment_bead.sh) <mount> <id> "text"
```
Label bead:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/label_bead.sh) <mount> <id> <label>
```
Delete bead:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/beads/delete_bead.sh) <mount> <id>
```
**Note:** Delete does not cascade. To delete a parent and all children, delete children first, then parent.

View File

@ -10,10 +10,10 @@ Import GitHub issues into beads.
## Usage
All commands must be run via `execute_code` tool.
```bash
github_to_beads.sh OWNER/REPO ISSUE_NUMBER
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/github/github_to_beads.sh) OWNER/REPO ISSUE_NUMBER
```
**What it does:**

View File

@ -10,10 +10,10 @@ Import Jira ticket hierarchies into beads.
## Usage
All commands must be run via `execute_code` tool.
```bash
jira_to_beads.sh PROJ-12345
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/jira/jira_to_beads.sh) PROJ-12345
```
**What it does:**

View File

@ -37,15 +37,33 @@ Load pcloudcc-testing skill. Test the affected code path with unit tests, fault
**Prerequisites**: Load `anvillm-sessions` and `anvillm-communication` skills
**Spawn testing agent**: `bash <(9p read agent/tools/agents/spawn_agent.sh) <your-id> "<system-prompt-above>"`
Spawn testing agent:
```
Tool: execute_code
sandbox: default
code: bash <(9p read agent/tools/agents/spawn_agent.sh) <your-id> "<system-prompt-above>"
```
**Send test request**: `bash <(9p read agent/tools/messaging/send_message.sh) <your-id> <testing-agent-id> PROMPT_REQUEST "Validate fix" "Bug: <desc>\nFix: <changes>\nFiles: <list>\nBranch: <name>"`
Send test request:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/messaging/send_message.sh) <your-id> <testing-agent-id> PROMPT_REQUEST "Validate fix" "Bug: <desc>\nFix: <changes>\nFiles: <list>\nBranch: <name>"
```
**Read response** (user notifies when ready): `bash <(9p read agent/tools/messaging/read_inbox.sh) <your-id>`
Read response (user notifies when ready):
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/messaging/read_inbox.sh) <your-id>
```
**Kill testing agent**: `bash <(9p read agent/tools/agents/kill_agent.sh) <testing-agent-id>`
**Note**: `spawn_agent.sh` is an exception — use the Bash tool (`execute_bash`), NOT `execute_code`. All other commands in this section use `execute_code` as normal.
Kill testing agent:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/agents/kill_agent.sh) <testing-agent-id>
```
### Testing Agent Workflow
@ -54,7 +72,12 @@ Load pcloudcc-testing skill. Test the affected code path with unit tests, fault
3. Design targeted test (reproduce bug scenario)
4. Execute test (unit/fault-inject/smoke)
5. Verify fix (old fails, new passes)
6. Report: `bash <(9p read agent/tools/messaging/send_message.sh) <your-id> <requester-id> PROMPT_RESPONSE "Validation complete" "Status: completed|failed\n<evidence>"`
6. Report:
```
Tool: execute_code
sandbox: anvilmcp
code: bash <(9p read agent/tools/messaging/send_message.sh) <your-id> <requester-id> PROMPT_RESPONSE "Validation complete" "Status: completed|failed\n<evidence>"
```
**FUSE testing**: Inform user daemon must start outside sandbox, wait for acknowledgement