diff --git a/.beads/config.json b/.beads/config.json new file mode 100644 index 0000000..8ac42b9 --- /dev/null +++ b/.beads/config.json @@ -0,0 +1 @@ +{"issue_prefix":"TST"} diff --git a/cmd/anvilmcp/execute.go b/cmd/anvilmcp/execute.go index e090f51..50c9b18 100644 --- a/cmd/anvilmcp/execute.go +++ b/cmd/anvilmcp/execute.go @@ -157,24 +157,26 @@ func loadLayeredConfig(name string) (*sandbox.Config, error) { return sandbox.Merge(general, advanced, layers...), nil } -func executeCode(code, language string, timeout int, sandboxName string) (string, error) { +func executeCode(code, language string, timeout int, sandboxName string, trusted bool) (string, error) { start := time.Now() if timeout <= 0 { timeout = 30 } - if err := validateCode(code); err != nil { - logExecution(ExecutionLog{ - Timestamp: start, - CodeHash: hashCode(code), - Language: language, - Duration: time.Since(start), - Success: false, - OutputSize: 0, - Error: err.Error(), - }) - return "", err + if !trusted { + if err := validateCode(code); err != nil { + logExecution(ExecutionLog{ + Timestamp: start, + CodeHash: hashCode(code), + Language: language, + Duration: time.Since(start), + Success: false, + OutputSize: 0, + Error: err.Error(), + }) + return "", err + } } // Load layered sandbox config @@ -256,8 +258,43 @@ func executeCode(code, language string, timeout int, sandboxName string) (string cmd = exec.CommandContext(ctx, landrunPath, args...) cmd.Dir = workDir + case "go": + args := []string{} + + addPaths := func(flag string, paths []string) { + for _, p := range paths { + expanded := sandbox.ExpandPath(p, workDir) + if expanded == "" || strings.Contains(expanded, "{") { + continue + } + if _, err := os.Stat(expanded); err == nil { + args = append(args, flag, expanded) + } + } + } + + addPaths("--ro", cfg.Filesystem.RO) + addPaths("--rox", cfg.Filesystem.ROX) + addPaths("--rw", cfg.Filesystem.RW) + addPaths("--rwx", cfg.Filesystem.RWX) + + if cfg.Network.Unrestricted { + args = append(args, "--unrestricted-network") + } + + for _, env := range cfg.Env { + if os.Getenv(env) != "" || env == "HOME" || env == "USER" || env == "PATH" { + args = append(args, "--env", env) + } + } + + args = append(args, "--", "go", "run", "-") + + cmd = exec.CommandContext(ctx, landrunPath, args...) + cmd.Dir = workDir + cmd.Stdin = strings.NewReader(code) default: - return "", fmt.Errorf("unsupported language: %s (only bash supported)", language) + return "", fmt.Errorf("unsupported language: %s (supported: bash, go)", language) } // Limit output size (10MB) diff --git a/cmd/anvilmcp/main.go b/cmd/anvilmcp/main.go index ba6e3de..efdf3f2 100644 --- a/cmd/anvilmcp/main.go +++ b/cmd/anvilmcp/main.go @@ -5,7 +5,11 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" + "strings" "time" + + "9fans.net/go/plan9/client" ) var ( @@ -54,6 +58,40 @@ type Items struct { Type string `json:"type"` } +// readTool reads a tool from the 9P tools directory +func readTool(name string) (string, error) { + // Prevent path traversal + if strings.Contains(name, "/") || strings.Contains(name, "..") { + return "", fmt.Errorf("invalid tool name") + } + + ns := fmt.Sprintf("/tmp/ns.%s.:0", os.Getenv("USER")) + fsys, err := client.Mount("unix", filepath.Join(ns, "agent")) + if err != nil { + return "", fmt.Errorf("failed to mount 9P: %v", err) + } + defer fsys.Close() + + fid, err := fsys.Open("/tools/"+name, 0) + if err != nil { + return "", fmt.Errorf("tool not found: %s", name) + } + defer fid.Close() + + var buf []byte + tmp := make([]byte, 8192) + for { + n, err := fid.Read(tmp) + if n > 0 { + buf = append(buf, tmp[:n]...) + } + if err != nil || n < len(tmp) { + break + } + } + return string(buf), nil +} + func main() { fmt.Fprintln(os.Stderr, "[anvilmcp] Starting MCP server") scanner := bufio.NewScanner(os.Stdin) @@ -88,16 +126,17 @@ func main() { "tools": []Tool{ { Name: "execute_code", - Description: "Execute bash code as a subprocess with timeout", + Description: "Execute code from tool library or inline", InputSchema: InputSchema{ Type: "object", Properties: map[string]Property{ - "code": {Type: "string", Description: "Bash code to execute"}, - "language": {Type: "string", Description: "Programming language (bash)", Enum: []string{"bash"}}, + "tool": {Type: "string", Description: "Tool name from agent/tools/ (e.g. 'list_sessions.sh')"}, + "args": {Type: "array", Description: "Arguments to pass to tool", Items: &Items{Type: "string"}}, + "code": {Type: "string", Description: "Inline code to execute (if tool not specified)"}, + "language": {Type: "string", Description: "Programming language (auto-detected from tool extension)", Enum: []string{"bash", "go"}}, "timeout": {Type: "integer", Description: "Timeout in seconds (default: 30)"}, "sandbox": {Type: "string", Description: "Sandbox config name (default: default)"}, }, - Required: []string{"code"}, }, }, }, @@ -124,8 +163,51 @@ func handleToolCall(req MCPRequest) { fmt.Fprintf(os.Stderr, "[anvilmcp] Tool call: %s with args: %v\n", params.Name, params.Arguments) switch params.Name { case "execute_code": + tool, _ := params.Arguments["tool"].(string) + var toolArgs []string + if argsRaw, ok := params.Arguments["args"].([]interface{}); ok { + for _, a := range argsRaw { + if s, ok := a.(string); ok { + toolArgs = append(toolArgs, s) + } + } + } code, _ := params.Arguments["code"].(string) language, _ := params.Arguments["language"].(string) + + // If tool specified, read from tools library + if tool != "" { + toolCode, err := readTool(tool) + if err != nil { + sendError(req.ID, -32000, fmt.Sprintf("failed to read tool %s: %v", tool, err)) + return + } + code = toolCode + // Auto-detect language from extension + if language == "" { + switch { + case strings.HasSuffix(tool, ".go"): + language = "go" + default: + language = "bash" + } + } + // For bash with args, wrap script to receive positional params + if language == "bash" && len(toolArgs) > 0 { + // Escape args for bash array + var escaped []string + for _, arg := range toolArgs { + escaped = append(escaped, fmt.Sprintf("%q", arg)) + } + code = fmt.Sprintf("set -- %s\n%s", strings.Join(escaped, " "), code) + } + } + + if code == "" { + sendError(req.ID, -32602, "either 'tool' or 'code' is required") + return + } + if language == "" { language = "bash" } @@ -143,13 +225,14 @@ func handleToolCall(req MCPRequest) { executionSemaphore <- struct{}{} defer func() { <-executionSemaphore }() - fmt.Fprintf(os.Stderr, "[anvilmcp] Executing bash code (timeout: %ds, sandbox: %s)\n", timeout, sandbox) - result, err := executeCode(code, language, timeout, sandbox) + trusted := tool != "" // Tools from 9P are trusted + fmt.Fprintf(os.Stderr, "[anvilmcp] Executing %s code (timeout: %ds, sandbox: %s, trusted: %v)\n", language, timeout, sandbox, trusted) + result, err := executeCode(code, language, timeout, sandbox, trusted) // Fallback to default sandbox on permission errors (only if sandbox wasn't explicit) if err != nil && !sandboxExplicit && isPermissionError(err) { fmt.Fprintf(os.Stderr, "[anvilmcp] Permission error in %s sandbox, falling back to default\n", sandbox) - result, err = executeCode(code, language, timeout, "default") + result, err = executeCode(code, language, timeout, "default", trusted) } // Log token comparison diff --git a/skills/anvillm-communication/SKILL.md b/skills/anvillm-communication/SKILL.md index dc8f9e4..b2209c3 100644 --- a/skills/anvillm-communication/SKILL.md +++ b/skills/anvillm-communication/SKILL.md @@ -14,22 +14,21 @@ when_to_load: Load when you need to send messages to other agents, respond to th Discover agents: ``` Tool: execute_code -sandbox: anvilmcp code: bash <(9p read agent/tools/list_sessions.sh) | grep "$(pwd)" ``` Send message: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/send_message.sh) +tool: send_message.sh +args: ["", "", "", "", ""] ``` Check inbox: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/read_inbox.sh) +tool: read_inbox.sh +args: [""] ``` ## Rules @@ -52,5 +51,3 @@ code: bash <(9p read agent/tools/read_inbox.sh) 1. Check your inbox 2. Follow the instructions in the message 3. Respond to the sender appropriately (see Sending Messages) - - diff --git a/skills/anvillm-sessions/SKILL.md b/skills/anvillm-sessions/SKILL.md index ae29f2e..45144fe 100644 --- a/skills/anvillm-sessions/SKILL.md +++ b/skills/anvillm-sessions/SKILL.md @@ -10,41 +10,39 @@ Create and control agent sessions. ## Commands -All commands use `execute_code` with `sandbox: "anvilmcp"`, EXCEPT spawn_agent.sh which requires `sandbox: "default"`. - List sessions: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/list_sessions.sh) +tool: list_sessions.sh ``` Create session: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/create_session.sh) [sandbox=] [model=] +tool: create_session.sh +args: ["", "", "sandbox=", "model="] ``` Control session: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/control_session.sh) +tool: control_session.sh +args: ["", ""] ``` -Spawn agent: +Spawn agent (requires default sandbox): ``` Tool: execute_code +tool: spawn_agent.sh +args: ["", "", ""] sandbox: default -code: bash <(9p read agent/tools/spawn_agent.sh) [cwd-path] [initial-context-prompt] ``` Kill agent: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/kill_agent.sh) [cwd-path] +tool: kill_agent.sh +args: [""] ``` ## Backends diff --git a/skills/beads/SKILL.md b/skills/beads/SKILL.md index de18579..4421852 100644 --- a/skills/beads/SKILL.md +++ b/skills/beads/SKILL.md @@ -11,15 +11,21 @@ description: Manage tasks using the beads 9P interface. Use when creating, updat Find your project's beads mount: ``` Tool: execute_code -sandbox: anvilmcp code: MOUNT=$(bash <(9p read agent/tools/list_mounts.sh) | grep "$(pwd)" | awk '{print $1}'); echo $MOUNT ``` -If no mount exists, create one: +If no mount exists, create one (substitute actual path): ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/mount_beads.sh) "$(pwd)" +tool: mount_beads.sh +args: ["/path/to/project"] +``` + +Initialize beads in a mount (creates .beads/): +``` +Tool: execute_code +tool: init_beads.sh +args: ["", ""] ``` ## Tools @@ -29,36 +35,35 @@ All tools require `` as first argument. Exceptions: `list_mounts.sh`, `mo List mounts: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/list_mounts.sh) +tool: list_mounts.sh ``` List ready beads: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/list_ready_beads.sh) +tool: list_ready_beads.sh +args: [""] ``` List all beads: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/list_beads.sh) +tool: list_beads.sh +args: [""] ``` Read bead JSON: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/read_bead.sh) json +tool: read_bead.sh +args: ["", "", "json"] ``` Read bead comments: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/read_bead.sh) comments +tool: read_bead.sh +args: ["", "", "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. @@ -66,64 +71,64 @@ code: bash <(9p read agent/tools/read_bead.sh) comments Claim bead: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/claim_bead.sh) [$AGENT_ID] +tool: claim_bead.sh +args: ["", "", "$AGENT_ID"] ``` Complete bead: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/complete_bead.sh) +tool: complete_bead.sh +args: ["", ""] ``` Fail bead: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/fail_bead.sh) "reason" +tool: fail_bead.sh +args: ["", "", ""] ``` Create bead: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/create_bead.sh) "title" "desc" [parent] +tool: create_bead.sh +args: ["", "", "<desc>", "<parent>"] ``` Update bead: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/update_bead.sh) <mount> <id> <field> <value> +tool: update_bead.sh +args: ["<mount>", "<id>", "<field>", "<value>"] ``` Add dependency: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/add_dependency.sh) <mount> <child> <parent> +tool: add_dependency.sh +args: ["<mount>", "<child>", "<parent>"] ``` Comment on bead: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/comment_bead.sh) <mount> <id> "text" +tool: comment_bead.sh +args: ["<mount>", "<id>", "<text>"] ``` Label bead: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/label_bead.sh) <mount> <id> <label> +tool: label_bead.sh +args: ["<mount>", "<id>", "<label>"] ``` Delete bead: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/delete_bead.sh) <mount> <id> +tool: delete_bead.sh +args: ["<mount>", "<id>"] ``` **Note:** Delete does not cascade. To delete a parent and all children, delete children first, then parent. diff --git a/skills/github-to-beads/SKILL.md b/skills/github-to-beads/SKILL.md index 2822ffb..8cb2013 100644 --- a/skills/github-to-beads/SKILL.md +++ b/skills/github-to-beads/SKILL.md @@ -12,8 +12,8 @@ Import GitHub issues into beads. ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/github_to_beads.sh) OWNER/REPO ISSUE_NUMBER +tool: github_to_beads.sh +args: ["<owner/repo>", "<issue_number>"] ``` **What it does:** diff --git a/skills/jira-to-beads/SKILL.md b/skills/jira-to-beads/SKILL.md index 17c45a8..ebbc5c2 100644 --- a/skills/jira-to-beads/SKILL.md +++ b/skills/jira-to-beads/SKILL.md @@ -12,8 +12,8 @@ Import Jira ticket hierarchies into beads. ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/jira_to_beads.sh) PROJ-12345 +tool: jira_to_beads.sh +args: ["<ticket-key>"] ``` **What it does:** diff --git a/skills/pcloudcc-testing/SKILL.md b/skills/pcloudcc-testing/SKILL.md index b7b06b6..a3b19c8 100644 --- a/skills/pcloudcc-testing/SKILL.md +++ b/skills/pcloudcc-testing/SKILL.md @@ -40,29 +40,30 @@ Load pcloudcc-testing skill. Test the affected code path with unit tests, fault Spawn testing agent: ``` Tool: execute_code +tool: spawn_agent.sh +args: ["<your-id>", "<cwd>", "<system-prompt-above>"] sandbox: default -code: bash <(9p read agent/tools/spawn_agent.sh) <your-id> "<system-prompt-above>" ``` Send test request: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/send_message.sh) <your-id> <testing-agent-id> PROMPT_REQUEST "Validate fix" "Bug: <desc>\nFix: <changes>\nFiles: <list>\nBranch: <name>" +tool: send_message.sh +args: ["<your-id>", "<testing-agent-id>", "PROMPT_REQUEST", "Validate fix", "Bug: <desc>\nFix: <changes>\nFiles: <list>\nBranch: <name>"] ``` Read response (user notifies when ready): ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/read_inbox.sh) <your-id> +tool: read_inbox.sh +args: ["<your-id>"] ``` Kill testing agent: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/kill_agent.sh) <testing-agent-id> +tool: kill_agent.sh +args: ["<testing-agent-id>"] ``` ### Testing Agent Workflow @@ -75,8 +76,8 @@ code: bash <(9p read agent/tools/kill_agent.sh) <testing-agent-id> 6. Report: ``` Tool: execute_code -sandbox: anvilmcp -code: bash <(9p read agent/tools/send_message.sh) <your-id> <requester-id> PROMPT_RESPONSE "Validation complete" "Status: completed|failed\n<evidence>" +tool: send_message.sh +args: ["<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