diff --git a/cfg/sandbox/anvilmcp.yaml b/cfg/sandbox/anvilmcp.yaml index 923738e..57e8127 100644 --- a/cfg/sandbox/anvilmcp.yaml +++ b/cfg/sandbox/anvilmcp.yaml @@ -5,6 +5,7 @@ filesystem: - "/etc/alternatives" - "/etc/ld.so.cache" - "/etc/passwd" + - "{HOME}/.local/share/anvillm/mail" - "/dev/urandom" rox: - "/usr" diff --git a/cfg/sandbox/default.yaml b/cfg/sandbox/default.yaml index 2d56430..bf0fdeb 100644 --- a/cfg/sandbox/default.yaml +++ b/cfg/sandbox/default.yaml @@ -5,6 +5,7 @@ filesystem: - "{XDG_CONFIG_HOME}/git" - "{HOME}/.ssh" - "{HOME}/.netrc" + - "{HOME}/.local/share/anvillm/mail" - "/etc/gitconfig" - "/etc/gitattributes" - "/etc/ssh/ssh_config" diff --git a/cmd/Assist/main.go b/cmd/Assist/main.go index 9c35351..16636ae 100644 --- a/cmd/Assist/main.go +++ b/cmd/Assist/main.go @@ -28,6 +28,11 @@ const windowName = "/AnviLLM/" // Terminal to use for tmux attach (configurable via environment or flag) var terminalCommand = getTerminalCommand() +// shellEscape escapes a string for use inside single quotes in shell commands. +func shellEscape(s string) string { + return strings.ReplaceAll(s, "'", "'\\''") +} + func getTerminalCommand() string { if term := os.Getenv("ANVILLM_TERMINAL"); term != "" { return term @@ -2506,7 +2511,7 @@ func createBeadFromMarkdown(content, parentID string, mount string) error { } // Build command: new 'title' 'description' [parent-id] [blockers=...] - cmd := fmt.Sprintf("new '%s' '%s'", title, description) + cmd := fmt.Sprintf("new '%s' '%s'", shellEscape(title), shellEscape(description)) if parentID != "" { cmd = fmt.Sprintf("%s %s", cmd, parentID) } @@ -2897,11 +2902,11 @@ func updateBead(beadID, content string, origBlockers []string, mount string) err // Update title and description if title != "" { - cmd := fmt.Sprintf("update %s title '%s'", beadID, title) + cmd := fmt.Sprintf("update %s title '%s'", beadID, shellEscape(title)) writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd)) } if description != "" { - cmd := fmt.Sprintf("update %s description '%s'", beadID, description) + cmd := fmt.Sprintf("update %s description '%s'", beadID, shellEscape(description)) writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd)) } diff --git a/cmd/anvilmcp/execute.go b/cmd/anvilmcp/execute.go index aaf36ea..16b1f5e 100644 --- a/cmd/anvilmcp/execute.go +++ b/cmd/anvilmcp/execute.go @@ -308,7 +308,8 @@ func executeCode(code, language string, timeout int, sandboxName string, trusted return "", fmt.Errorf("unsupported language: %s (supported: bash)", language) } - // Limit output size (10MB) + // Limit output size (10MB raw, 8KB returned) + const maxToolOutputSize = 8000 var outputBuf bytes.Buffer lw := &limitedWriter{w: &outputBuf, limit: 10 * 1024 * 1024} cmd.Stdout = lw @@ -350,7 +351,11 @@ func executeCode(code, language string, timeout int, sandboxName string, trusted } logExecution(execLog) - return string(output), nil + result := string(output) + if len(result) > maxToolOutputSize { + result = result[:maxToolOutputSize] + "\n... (output truncated)" + } + return result, nil } type limitedWriter struct { diff --git a/internal/p9/beads.go b/internal/p9/beads.go index 3cdca55..43b0dce 100644 --- a/internal/p9/beads.go +++ b/internal/p9/beads.go @@ -472,7 +472,7 @@ func (b *BeadsFS) readListFromStore(store bd.Storage, limit int) ([]byte, error) } // Sort by ID for hierarchical view (bd-a3f8, bd-a3f8.1, bd-a3f8.1.1, etc.) sort.Slice(issues, func(i, j int) bool { - return issues[i].ID < issues[j].ID + return naturalLess(issues[i].ID, issues[j].ID) }) // Enrich with blockers, labels, and capability level. @@ -510,6 +510,48 @@ func (b *BeadsFS) readListFromStore(store bd.Storage, limit int) ([]byte, error) return json.MarshalIndent(items, "", " ") } +// naturalLess compares two strings with natural number ordering. +// "jira-100.2" < "jira-100.10" (unlike lexicographic where .10 < .2) +func naturalLess(a, b string) bool { + for { + if a == "" { + return b != "" + } + if b == "" { + return false + } + // Find leading non-digit prefix + ai, bi := 0, 0 + for ai < len(a) && (a[ai] < '0' || a[ai] > '9') { + ai++ + } + for bi < len(b) && (b[bi] < '0' || b[bi] > '9') { + bi++ + } + if a[:ai] != b[:bi] { + return a[:ai] < b[:bi] + } + a, b = a[ai:], b[bi:] + if a == "" || b == "" { + continue + } + // Extract numeric part + ai, bi = 0, 0 + for ai < len(a) && a[ai] >= '0' && a[ai] <= '9' { + ai++ + } + for bi < len(b) && b[bi] >= '0' && b[bi] <= '9' { + bi++ + } + na, _ := strconv.Atoi(a[:ai]) + nb, _ := strconv.Atoi(b[:bi]) + if na != nb { + return na < nb + } + a, b = a[ai:], b[bi:] + } +} + func (b *BeadsFS) readReadyFromStore(store bd.Storage, limit int) ([]byte, error) { filter := bd.WorkFilter{Limit: limit} diff --git a/mkfile b/mkfile index e4fcc3f..15d6f73 100644 --- a/mkfile +++ b/mkfile @@ -6,16 +6,12 @@ build:V: go build -o $INSTALL_PATH/anvilsrv ./cmd/anvilsrv go build -o $INSTALL_PATH/Assist ./cmd/Assist go build -o $INSTALL_PATH/anvilmcp ./cmd/anvilmcp - cp scripts/Alog $INSTALL_PATH/Alog - chmod 0755 $INSTALL_PATH/Alog cp scripts/anvillm-notify $INSTALL_PATH/anvillm-notify chmod 0755 $INSTALL_PATH/anvillm-notify cp scripts/anvillm-skills $INSTALL_PATH/anvillm-skills chmod 0755 $INSTALL_PATH/anvillm-skills cp scripts/anvillm-hook $INSTALL_PATH/anvillm-hook chmod 0755 $INSTALL_PATH/anvillm-hook - cp scripts/9p-read-inbox $INSTALL_PATH/9p-read-inbox - chmod 0755 $INSTALL_PATH/9p-read-inbox cp scripts/anvilspawn $INSTALL_PATH/anvilspawn chmod 0755 $INSTALL_PATH/anvilspawn cp scripts/anvillm-supervisor $INSTALL_PATH/anvillm-supervisor