fix(cli): confine agent runtime paths

This commit is contained in:
ZacharyZhang-NY committed 2026-07-19 16:26:00 -04:00
1 parent c9d1075585
commit 7bf84a35e0
10 files changed
+399 -51

No files matched your search

+1
View File
@@ -40,6 +40,7 @@ This repository owns the public Go SDK, the `cmd/elydora` CLI, local signing beh
- Return unexpected errors to the CLI boundary with operation and path context. - Return unexpected errors to the CLI boundary with operation and path context.
- Use documented defaults only for genuinely optional configuration. - Use documented defaults only for genuinely optional configuration.
- Avoid compatibility shims without a named public or user configuration contract. - Avoid compatibility shims without a named public or user configuration contract.
- Resolve every agent runtime directory as one physical child of `~/.elydora`; reject separators, traversal segments, cross-platform reserved names, and symbolic-link directories before writes or recursive removal.
## Verification ## Verification
+2
View File
@@ -63,6 +63,8 @@ func main() {
The SDK includes a CLI for installing audit hooks into AI coding agents. The SDK includes a CLI for installing audit hooks into AI coding agents.
Agent IDs map to one physical directory directly under `~/.elydora`; path separators, traversal segments, Windows reserved names, and symbolic-link directories are rejected.
```bash ```bash
go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest
+74 -30
View File
@@ -92,16 +92,13 @@ func cmdInstall(args []string) {
} }
// Generate guard script (PreToolUse — freeze enforcement) // Generate guard script (PreToolUse — freeze enforcement)
guardScriptPath, err := guardScriptPathForAgent(*agentID) agentDirectory, err := plugins.PrepareAgentRuntimeDirectory(*agentID)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1) os.Exit(1)
} }
guardScriptPath := filepath.Join(agentDirectory, "guard.js")
guardScript := plugins.GenerateGuardScript(*agent, *agentID) guardScript := plugins.GenerateGuardScript(*agent, *agentID)
if err := os.MkdirAll(filepath.Dir(guardScriptPath), 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating guard script directory: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(guardScriptPath, []byte(guardScript), 0755); err != nil { if err := os.WriteFile(guardScriptPath, []byte(guardScript), 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error writing guard script: %v\n", err) fmt.Fprintf(os.Stderr, "Error writing guard script: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -149,13 +146,38 @@ func cmdUninstall(args []string) {
os.Exit(1) os.Exit(1)
} }
runtimeRoot, err := plugins.AgentRuntimeRoot()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if _, err := plugins.RequirePhysicalDirectory(runtimeRoot); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Resolve agentID: if not given, scan ~/.elydora/*/config.json for matching agent_name // Resolve agentID: if not given, scan ~/.elydora/*/config.json for matching agent_name
resolvedAgentID := *agentID resolvedAgentID := *agentID
if resolvedAgentID == "" { if resolvedAgentID == "" {
resolvedAgentID = findAgentIDByName(*agent) resolvedAgentID, err = findAgentIDByName(*agent)
if resolvedAgentID == "" { if err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not find agent ID for %q in ~/.elydora/*/config.json\n", *agent) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
if resolvedAgentID == "" {
fmt.Fprintf(os.Stderr, "Error: could not find agent ID for %q in ~/.elydora/*/config.json; pass --agent-id explicitly\n", *agent)
os.Exit(1)
}
}
agentDirectory, err := plugins.ResolveAgentRuntimeDirectory(resolvedAgentID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
agentDirectoryExists, err := plugins.RequirePhysicalDirectory(agentDirectory)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
if err := plugin.Uninstall(resolvedAgentID); err != nil { if err := plugin.Uninstall(resolvedAgentID); err != nil {
@@ -164,49 +186,71 @@ func cmdUninstall(args []string) {
} }
// Remove the per-agent directory // Remove the per-agent directory
if resolvedAgentID != "" { if agentDirectoryExists {
home, err := os.UserHomeDir() if err := os.RemoveAll(agentDirectory); err != nil {
if err == nil { fmt.Fprintf(os.Stderr, "Error removing agent directory %s: %v\n", agentDirectory, err)
agentDir := filepath.Join(home, ".elydora", resolvedAgentID) os.Exit(1)
if err := os.RemoveAll(agentDir); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to remove agent directory %s: %v\n", agentDir, err)
} else {
fmt.Printf(" Removed agent directory: %s\n", agentDir)
}
} }
fmt.Printf(" Removed agent directory: %s\n", agentDirectory)
} }
} }
// findAgentIDByName scans ~/.elydora/*/config.json for a config whose agent_name // findAgentIDByName scans ~/.elydora/*/config.json for a config whose agent_name
// matches the given name and returns the directory name (agent ID). // matches the given name and returns the directory name (agent ID).
func findAgentIDByName(agentName string) string { func findAgentIDByName(agentName string) (string, error) {
home, err := os.UserHomeDir() elydoraDir, err := plugins.AgentRuntimeRoot()
if err != nil { if err != nil {
return "" return "", err
}
exists, err := plugins.RequirePhysicalDirectory(elydoraDir)
if err != nil {
return "", err
}
if !exists {
return "", nil
} }
elydoraDir := filepath.Join(home, ".elydora")
entries, err := os.ReadDir(elydoraDir) entries, err := os.ReadDir(elydoraDir)
if err != nil { if err != nil {
return "" return "", fmt.Errorf("read Elydora runtime root %s: %w", elydoraDir, err)
} }
foundAgentID := ""
for _, entry := range entries { for _, entry := range entries {
if !entry.IsDir() { if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() {
continue continue
} }
configPath := filepath.Join(elydoraDir, entry.Name(), "config.json") configPath := filepath.Join(elydoraDir, entry.Name(), "config.json")
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
if err != nil { if os.IsNotExist(err) {
continue continue
} }
if err != nil {
return "", fmt.Errorf("read agent config %s: %w", configPath, err)
}
var config map[string]interface{} var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil { if err := json.Unmarshal(data, &config); err != nil {
continue return "", fmt.Errorf("parse agent config %s: %w", configPath, err)
} }
if name, _ := config["agent_name"].(string); name == agentName { if name, _ := config["agent_name"].(string); name == agentName {
return entry.Name() storedAgentID, ok := config["agent_id"].(string)
if !ok || storedAgentID == "" {
return "", fmt.Errorf("agent config %s has an invalid agent_id", configPath)
}
if storedAgentID != entry.Name() {
return "", fmt.Errorf("agent config %s crosses its runtime directory", configPath)
}
if _, err := plugins.ResolveAgentRuntimeDirectory(storedAgentID); err != nil {
return "", fmt.Errorf("validate agent config %s: %w", configPath, err)
}
if foundAgentID != "" {
return "", fmt.Errorf(
"multiple %q agent runtimes found; pass --agent-id explicitly",
agentName,
)
}
foundAgentID = storedAgentID
} }
} }
return "" return foundAgentID, nil
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -285,9 +329,9 @@ func sortedAgentNames() []string {
// guardScriptPathForAgent returns the path to ~/.elydora/<agentId>/guard.js. // guardScriptPathForAgent returns the path to ~/.elydora/<agentId>/guard.js.
func guardScriptPathForAgent(agentId string) (string, error) { func guardScriptPathForAgent(agentId string) (string, error) {
home, err := os.UserHomeDir() agentDirectory, err := plugins.ResolveAgentRuntimeDirectory(agentId)
if err != nil { if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err) return "", err
} }
return filepath.Join(home, ".elydora", agentId, "guard.js"), nil return filepath.Join(agentDirectory, "guard.js"), nil
} }
+80
View File
@@ -0,0 +1,80 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func setMainTestHome(t *testing.T) string {
t.Helper()
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
return home
}
func TestGuardScriptPathRejectsUnsafeAgentID(t *testing.T) {
setMainTestHome(t)
_, err := guardScriptPathForAgent("../escape")
if err == nil || !strings.Contains(err.Error(), "invalid agent ID") {
t.Fatalf("guardScriptPathForAgent() error = %v", err)
}
}
func TestFindAgentIDByNameReturnsValidatedDirectoryIdentity(t *testing.T) {
home := setMainTestHome(t)
runtimeDirectory := filepath.Join(home, ".elydora", "agent-1")
if err := os.MkdirAll(runtimeDirectory, 0700); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
config := []byte(`{"agent_name":"opencode","agent_id":"agent-1"}`)
if err := os.WriteFile(filepath.Join(runtimeDirectory, "config.json"), config, 0600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
agentID, err := findAgentIDByName("opencode")
if err != nil {
t.Fatalf("findAgentIDByName() error = %v", err)
}
if agentID != "agent-1" {
t.Fatalf("agent ID = %q, want agent-1", agentID)
}
}
func TestFindAgentIDByNameRejectsConfigDirectoryMismatch(t *testing.T) {
home := setMainTestHome(t)
runtimeDirectory := filepath.Join(home, ".elydora", "stored-directory")
if err := os.MkdirAll(runtimeDirectory, 0700); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
config := []byte(`{"agent_name":"opencode","agent_id":"different-agent"}`)
if err := os.WriteFile(filepath.Join(runtimeDirectory, "config.json"), config, 0600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
_, err := findAgentIDByName("opencode")
if err == nil || !strings.Contains(err.Error(), "crosses its runtime directory") {
t.Fatalf("findAgentIDByName() error = %v", err)
}
}
func TestFindAgentIDByNameRejectsAmbiguousRuntimes(t *testing.T) {
home := setMainTestHome(t)
for _, agentID := range []string{"agent-1", "agent-2"} {
runtimeDirectory := filepath.Join(home, ".elydora", agentID)
if err := os.MkdirAll(runtimeDirectory, 0700); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
config := []byte(`{"agent_name":"opencode","agent_id":"` + agentID + `"}`)
if err := os.WriteFile(filepath.Join(runtimeDirectory, "config.json"), config, 0600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
}
_, err := findAgentIDByName("opencode")
if err == nil || !strings.Contains(err.Error(), "multiple") {
t.Fatalf("findAgentIDByName() error = %v", err)
}
}
+5 -4
View File
@@ -153,14 +153,15 @@ func (p *DroidPlugin) Status() (PluginStatus, error) {
} }
func droidAgentDirectory(agentID string) (string, string, error) { func droidAgentDirectory(agentID string) (string, string, error) {
if agentID == "" || agentID == "." || agentID == ".." || filepath.IsAbs(agentID) || filepath.Base(agentID) != agentID {
return "", "", fmt.Errorf("agent_id must be a single non-empty path segment")
}
runtimeRoot, err := droidRuntimeRoot() runtimeRoot, err := droidRuntimeRoot()
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
return runtimeRoot, filepath.Join(runtimeRoot, agentID), nil agentDirectory, err := ResolveAgentRuntimeDirectory(agentID)
if err != nil {
return "", "", err
}
return runtimeRoot, agentDirectory, nil
} }
func droidSettingsDocument(settings *droidDocument) *droidDocument { func droidSettingsDocument(settings *droidDocument) *droidDocument {
+3 -7
View File
@@ -12,7 +12,7 @@ import (
// It always exits 0 so it never blocks the host agent. // It always exits 0 so it never blocks the host agent.
func GenerateHookScript(destPath string, config InstallConfig) error { func GenerateHookScript(destPath string, config InstallConfig) error {
dir := filepath.Dir(destPath) dir := filepath.Dir(destPath)
if err := os.MkdirAll(dir, 0755); err != nil { if err := EnsurePrivateDirectory(dir); err != nil {
return fmt.Errorf("create hook script directory: %w", err) return fmt.Errorf("create hook script directory: %w", err)
} }
@@ -32,13 +32,9 @@ func GenerateHookScript(destPath string, config InstallConfig) error {
// ~/.elydora/<agentId>/config.json and ~/.elydora/<agentId>/private.key so that // ~/.elydora/<agentId>/config.json and ~/.elydora/<agentId>/private.key so that
// the generated hook script can read them at runtime without embedding secrets. // the generated hook script can read them at runtime without embedding secrets.
func writeAgentConfig(config InstallConfig) error { func writeAgentConfig(config InstallConfig) error {
home, err := os.UserHomeDir() agentDir, err := PrepareAgentRuntimeDirectory(config.AgentID)
if err != nil { if err != nil {
return fmt.Errorf("resolve home directory: %w", err) return err
}
agentDir := filepath.Join(home, ".elydora", config.AgentID)
if err := os.MkdirAll(agentDir, 0755); err != nil {
return fmt.Errorf("create agent directory: %w", err)
} }
baseURL := config.BaseURL baseURL := config.BaseURL
+6 -6
View File
@@ -55,18 +55,18 @@ func writeJSONFile(path string, data map[string]interface{}) error {
// hookScriptPath returns the default path for the hook script inside ~/.elydora/<agentId>/. // hookScriptPath returns the default path for the hook script inside ~/.elydora/<agentId>/.
func hookScriptPath(agentId string) (string, error) { func hookScriptPath(agentId string) (string, error) {
home, err := os.UserHomeDir() agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
if err != nil { if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err) return "", err
} }
return filepath.Join(home, ".elydora", agentId, "hook.js"), nil return filepath.Join(agentDirectory, "hook.js"), nil
} }
// guardScriptPath returns the default path for the guard script inside ~/.elydora/<agentId>/. // guardScriptPath returns the default path for the guard script inside ~/.elydora/<agentId>/.
func guardScriptPath(agentId string) (string, error) { func guardScriptPath(agentId string) (string, error) {
home, err := os.UserHomeDir() agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
if err != nil { if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err) return "", err
} }
return filepath.Join(home, ".elydora", agentId, "guard.js"), nil return filepath.Join(agentDirectory, "guard.js"), nil
} }
+5 -4
View File
@@ -119,14 +119,15 @@ func (p *QwenPlugin) Status() (PluginStatus, error) {
} }
func qwenAgentDirectory(agentID string) (string, string, error) { func qwenAgentDirectory(agentID string) (string, string, error) {
if agentID == "" || agentID == "." || agentID == ".." || filepath.IsAbs(agentID) || filepath.Base(agentID) != agentID {
return "", "", fmt.Errorf("agent_id must be a single non-empty path segment")
}
runtimeRoot, err := qwenRuntimeRoot() runtimeRoot, err := qwenRuntimeRoot()
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
return runtimeRoot, filepath.Join(runtimeRoot, agentID), nil agentDirectory, err := ResolveAgentRuntimeDirectory(agentID)
if err != nil {
return "", "", err
}
return runtimeRoot, agentDirectory, nil
} }
func prepareQwenInstallationChanges( func prepareQwenInstallationChanges(
+128
View File
@@ -0,0 +1,128 @@
package plugins
import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
)
var windowsDeviceName = regexp.MustCompile(
`(?i)^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)`,
)
// AgentRuntimeRoot returns the local root for all Elydora agent runtimes.
func AgentRuntimeRoot() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
root, err := filepath.Abs(filepath.Join(home, ".elydora"))
if err != nil {
return "", fmt.Errorf("resolve Elydora runtime root: %w", err)
}
return root, nil
}
// ResolveAgentRuntimeDirectory confines an agent ID to one portable path segment.
func ResolveAgentRuntimeDirectory(agentID string) (string, error) {
if invalidAgentDirectoryName(agentID) {
return "", fmt.Errorf(
"invalid agent ID for local storage: %q must be a single non-empty path segment with a portable file name",
agentID,
)
}
root, err := AgentRuntimeRoot()
if err != nil {
return "", err
}
candidate := filepath.Join(root, agentID)
relative, err := filepath.Rel(root, candidate)
if err != nil {
return "", fmt.Errorf("resolve agent runtime path for %q: %w", agentID, err)
}
if relative == "." || relative == ".." || filepath.IsAbs(relative) ||
strings.HasPrefix(relative, ".."+string(os.PathSeparator)) ||
strings.ContainsRune(relative, os.PathSeparator) {
return "", fmt.Errorf("agent ID escapes the local storage directory: %q", agentID)
}
if _, err := RequirePhysicalDirectory(root); err != nil {
return "", err
}
if _, err := RequirePhysicalDirectory(candidate); err != nil {
return "", err
}
return candidate, nil
}
func invalidAgentDirectoryName(agentID string) bool {
if agentID == "" || agentID == "." || agentID == ".." ||
strings.ContainsAny(agentID, `<>:"/\|?*`) ||
strings.HasPrefix(agentID, " ") || strings.HasSuffix(agentID, ".") ||
strings.HasSuffix(agentID, " ") ||
windowsDeviceName.MatchString(agentID) {
return true
}
for _, character := range agentID {
if character <= 31 {
return true
}
}
return false
}
// EnsurePrivateDirectory creates an owner-only physical directory.
func EnsurePrivateDirectory(path string) error {
if err := os.MkdirAll(path, 0700); err != nil {
return fmt.Errorf("create private directory %s: %w", path, err)
}
info, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("inspect private directory %s: %w", path, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return fmt.Errorf("private directory path is not a physical directory: %s", path)
}
if runtime.GOOS != "windows" {
if err := os.Chmod(path, 0700); err != nil {
return fmt.Errorf("restrict private directory %s: %w", path, err)
}
}
return nil
}
// PrepareAgentRuntimeDirectory validates and creates the private runtime path.
func PrepareAgentRuntimeDirectory(agentID string) (string, error) {
root, err := AgentRuntimeRoot()
if err != nil {
return "", err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(agentID)
if err != nil {
return "", err
}
if err := EnsurePrivateDirectory(root); err != nil {
return "", err
}
if err := EnsurePrivateDirectory(agentDirectory); err != nil {
return "", err
}
return agentDirectory, nil
}
// RequirePhysicalDirectory reports whether a path is an existing physical directory.
func RequirePhysicalDirectory(path string) (bool, error) {
info, err := os.Lstat(path)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("inspect agent runtime directory %s: %w", path, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return false, fmt.Errorf("agent runtime path is not a physical directory: %s", path)
}
return true, nil
}
+95
View File
@@ -0,0 +1,95 @@
package plugins
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func setRuntimeTestHome(t *testing.T) string {
t.Helper()
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
return home
}
func TestResolveAgentRuntimeDirectoryRejectsUnsafePortableNames(t *testing.T) {
setRuntimeTestHome(t)
invalid := []string{
"../escape",
`..\escape`,
"C:escape",
"agent.",
"agent ",
" agent",
"CON",
"COM¹.log",
".",
"..",
}
for _, agentID := range invalid {
t.Run(agentID, func(t *testing.T) {
_, err := ResolveAgentRuntimeDirectory(agentID)
if err == nil || !strings.Contains(err.Error(), "invalid agent ID") {
t.Fatalf("ResolveAgentRuntimeDirectory(%q) error = %v", agentID, err)
}
})
}
}
func TestPrepareAgentRuntimeDirectoryCreatesPrivatePhysicalChild(t *testing.T) {
home := setRuntimeTestHome(t)
agentDirectory, err := PrepareAgentRuntimeDirectory("agent-1")
if err != nil {
t.Fatalf("PrepareAgentRuntimeDirectory() error = %v", err)
}
want := filepath.Join(home, ".elydora", "agent-1")
if agentDirectory != want {
t.Fatalf("agent directory = %q, want %q", agentDirectory, want)
}
for _, directory := range []string{filepath.Dir(agentDirectory), agentDirectory} {
info, err := os.Lstat(directory)
if err != nil {
t.Fatalf("Lstat(%q) error = %v", directory, err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
t.Fatalf("%q is not a physical directory", directory)
}
if runtime.GOOS != "windows" && info.Mode().Perm() != 0700 {
t.Fatalf("%q mode = %o, want 700", directory, info.Mode().Perm())
}
}
}
func TestPrepareAgentRuntimeDirectoryRejectsSymlinkRoot(t *testing.T) {
home := setRuntimeTestHome(t)
target := t.TempDir()
root := filepath.Join(home, ".elydora")
if err := os.Symlink(target, root); err != nil {
t.Skipf("directory symbolic links unavailable: %v", err)
}
_, err := PrepareAgentRuntimeDirectory("agent-1")
if err == nil || !strings.Contains(err.Error(), "physical directory") {
t.Fatalf("PrepareAgentRuntimeDirectory() error = %v", err)
}
}
func TestResolveAgentRuntimeDirectoryRejectsSymlinkChild(t *testing.T) {
home := setRuntimeTestHome(t)
root := filepath.Join(home, ".elydora")
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
target := t.TempDir()
link := filepath.Join(root, "agent-1")
if err := os.Symlink(target, link); err != nil {
t.Skipf("directory symbolic links unavailable: %v", err)
}
_, err := ResolveAgentRuntimeDirectory("agent-1")
if err == nil || !strings.Contains(err.Error(), "physical directory") {
t.Fatalf("ResolveAgentRuntimeDirectory() error = %v", err)
}
}