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

+5 -4
View File
@@ -153,14 +153,15 @@ func (p *DroidPlugin) Status() (PluginStatus, 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()
if err != nil {
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 {
+3 -7
View File
@@ -12,7 +12,7 @@ import (
// It always exits 0 so it never blocks the host agent.
func GenerateHookScript(destPath string, config InstallConfig) error {
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)
}
@@ -32,13 +32,9 @@ func GenerateHookScript(destPath string, config InstallConfig) error {
// ~/.elydora/<agentId>/config.json and ~/.elydora/<agentId>/private.key so that
// the generated hook script can read them at runtime without embedding secrets.
func writeAgentConfig(config InstallConfig) error {
home, err := os.UserHomeDir()
agentDir, err := PrepareAgentRuntimeDirectory(config.AgentID)
if err != nil {
return fmt.Errorf("resolve home directory: %w", err)
}
agentDir := filepath.Join(home, ".elydora", config.AgentID)
if err := os.MkdirAll(agentDir, 0755); err != nil {
return fmt.Errorf("create agent directory: %w", err)
return err
}
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>/.
func hookScriptPath(agentId string) (string, error) {
home, err := os.UserHomeDir()
agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
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>/.
func guardScriptPath(agentId string) (string, error) {
home, err := os.UserHomeDir()
agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
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) {
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()
if err != nil {
return "", "", err
}
return runtimeRoot, filepath.Join(runtimeRoot, agentID), nil
agentDirectory, err := ResolveAgentRuntimeDirectory(agentID)
if err != nil {
return "", "", err
}
return runtimeRoot, agentDirectory, nil
}
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)
}
}