fix(cli): confine agent runtime paths
This commit is contained in:
1 parent
c9d1075585
commit
7bf84a35e0
10 files changed
+399
-51
No files matched your search
@@ -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.
|
||||
- Use documented defaults only for genuinely optional configuration.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ func main() {
|
||||
|
||||
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
|
||||
go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest
|
||||
|
||||
|
||||
+74
-30
@@ -92,16 +92,13 @@ func cmdInstall(args []string) {
|
||||
}
|
||||
|
||||
// Generate guard script (PreToolUse — freeze enforcement)
|
||||
guardScriptPath, err := guardScriptPathForAgent(*agentID)
|
||||
agentDirectory, err := plugins.PrepareAgentRuntimeDirectory(*agentID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
guardScriptPath := filepath.Join(agentDirectory, "guard.js")
|
||||
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 {
|
||||
fmt.Fprintf(os.Stderr, "Error writing guard script: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -149,13 +146,38 @@ func cmdUninstall(args []string) {
|
||||
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
|
||||
resolvedAgentID := *agentID
|
||||
if resolvedAgentID == "" {
|
||||
resolvedAgentID = findAgentIDByName(*agent)
|
||||
if resolvedAgentID == "" {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not find agent ID for %q in ~/.elydora/*/config.json\n", *agent)
|
||||
resolvedAgentID, err = findAgentIDByName(*agent)
|
||||
if err != nil {
|
||||
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 {
|
||||
@@ -164,49 +186,71 @@ func cmdUninstall(args []string) {
|
||||
}
|
||||
|
||||
// Remove the per-agent directory
|
||||
if resolvedAgentID != "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
agentDir := filepath.Join(home, ".elydora", resolvedAgentID)
|
||||
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)
|
||||
}
|
||||
if agentDirectoryExists {
|
||||
if err := os.RemoveAll(agentDirectory); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error removing agent directory %s: %v\n", agentDirectory, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" Removed agent directory: %s\n", agentDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
// findAgentIDByName scans ~/.elydora/*/config.json for a config whose agent_name
|
||||
// matches the given name and returns the directory name (agent ID).
|
||||
func findAgentIDByName(agentName string) string {
|
||||
home, err := os.UserHomeDir()
|
||||
func findAgentIDByName(agentName string) (string, error) {
|
||||
elydoraDir, err := plugins.AgentRuntimeRoot()
|
||||
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)
|
||||
if err != nil {
|
||||
return ""
|
||||
return "", fmt.Errorf("read Elydora runtime root %s: %w", elydoraDir, err)
|
||||
}
|
||||
foundAgentID := ""
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
configPath := filepath.Join(elydoraDir, entry.Name(), "config.json")
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read agent config %s: %w", configPath, err)
|
||||
}
|
||||
var config map[string]interface{}
|
||||
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 {
|
||||
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.
|
||||
func guardScriptPathForAgent(agentId string) (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
agentDirectory, err := plugins.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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user