Adds the Clotho dual-run verification system and hardens the entire agent dispatch pipeline against command injection, token exposure, and SSH MitM attacks. Breaks the agentci→handlers circular dependency. Security: - SanitizePath (regex whitelist + filepath.Base) for all dispatch inputs - EscapeShellArg for shell argument safety - SecureSSHCommand (StrictHostKeyChecking=yes, BatchMode=yes) - ForgeToken removed from ticket JSON, transferred via .env with 0600 - ssh-keyscan on agent add populates known_hosts before first connection Clotho: - Spinner orchestrator determines Standard vs Dual execution mode - Config-driven via ClothoConfig (strategy, validation_threshold) - Agent runner supports claude/codex/gemini backends with dual-run - Divergence detection compares thread outputs via git diff API: - LoadActiveAgents() returns map[string]AgentConfig (no handlers import) - LoadClothoConfig() reads clotho section from config - Forge helpers: AssignIssue, EnsureLabel, AddIssueLabels 32 tests pass (19 agentci + 13 dispatch). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
49 lines
1.4 KiB
Go
49 lines
1.4 KiB
Go
package agentci
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`)
|
|
|
|
// SanitizePath ensures a filename or directory name is safe and prevents path traversal.
|
|
// Returns filepath.Base of the input after validation.
|
|
func SanitizePath(input string) (string, error) {
|
|
base := filepath.Base(input)
|
|
if !safeNameRegex.MatchString(base) {
|
|
return "", fmt.Errorf("invalid characters in path element: %s", input)
|
|
}
|
|
if base == "." || base == ".." || base == "/" {
|
|
return "", fmt.Errorf("invalid path element: %s", base)
|
|
}
|
|
return base, nil
|
|
}
|
|
|
|
// EscapeShellArg wraps a string in single quotes for safe remote shell insertion.
|
|
// Prefer exec.Command arguments over constructing shell strings where possible.
|
|
func EscapeShellArg(arg string) string {
|
|
return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
|
}
|
|
|
|
// SecureSSHCommand creates an SSH exec.Cmd with strict host key checking and batch mode.
|
|
func SecureSSHCommand(host string, remoteCmd string) *exec.Cmd {
|
|
return exec.Command("ssh",
|
|
"-o", "StrictHostKeyChecking=yes",
|
|
"-o", "BatchMode=yes",
|
|
"-o", "ConnectTimeout=10",
|
|
host,
|
|
remoteCmd,
|
|
)
|
|
}
|
|
|
|
// MaskToken returns a masked version of a token for safe logging.
|
|
func MaskToken(token string) string {
|
|
if len(token) < 8 {
|
|
return "*****"
|
|
}
|
|
return token[:4] + "****" + token[len(token)-4:]
|
|
}
|