fix(cli): validate runtimes before uninstall
This commit is contained in:
1 parent
7bf84a35e0
commit
3488ab778c
7 files changed
+250
-87
No files matched your search
@@ -40,7 +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.
|
- Resolve every agent runtime directory as one physical child of `~/.elydora`; reject separators, traversal segments, cross-platform reserved names, symbolic-link directories, and linked identity configs before writes or recursive removal. Validate stored directory identity before changing host CLI configuration, and require an explicit agent ID when discovery is ambiguous.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ 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.
|
Agent IDs map to one physical directory directly under `~/.elydora`; portable filename rules and physical directory/config checks apply before writes or recursive removal. Ambiguous uninstall discovery requires an explicit agent ID.
|
||||||
|
|
||||||
```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
|
||||||
|
|||||||
+2
-85
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -146,37 +145,13 @@ func cmdUninstall(args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
runtimeRoot, err := plugins.AgentRuntimeRoot()
|
resolvedAgentID, agentDirectory, agentDirectoryExists, err := resolveAgentRuntimeForUninstall(*agent, *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)
|
||||||
}
|
}
|
||||||
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 == "" {
|
if resolvedAgentID == "" {
|
||||||
resolvedAgentID, err = findAgentIDByName(*agent)
|
fmt.Fprintf(os.Stderr, "Error: could not find agent ID for %q in ~/.elydora/*/config.json; pass --agent-id explicitly\n", *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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,64 +170,6 @@ func cmdUninstall(args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, error) {
|
|
||||||
elydoraDir, err := plugins.AgentRuntimeRoot()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
exists, err := plugins.RequirePhysicalDirectory(elydoraDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
entries, err := os.ReadDir(elydoraDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read Elydora runtime root %s: %w", elydoraDir, err)
|
|
||||||
}
|
|
||||||
foundAgentID := ""
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
configPath := filepath.Join(elydoraDir, entry.Name(), "config.json")
|
|
||||||
data, err := os.ReadFile(configPath)
|
|
||||||
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 {
|
|
||||||
return "", fmt.Errorf("parse agent config %s: %w", configPath, err)
|
|
||||||
}
|
|
||||||
if name, _ := config["agent_name"].(string); name == agentName {
|
|
||||||
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 foundAgentID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// status
|
// status
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -78,3 +78,59 @@ func TestFindAgentIDByNameRejectsAmbiguousRuntimes(t *testing.T) {
|
|||||||
t.Fatalf("findAgentIDByName() error = %v", err)
|
t.Fatalf("findAgentIDByName() error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExplicitUninstallRejectsAgentOwnershipMismatch(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":"codex","agent_id":"agent-1"}`)
|
||||||
|
if err := os.WriteFile(filepath.Join(runtimeDirectory, "config.json"), config, 0600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, err := resolveAgentRuntimeForUninstall("opencode", "agent-1")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "belongs to") {
|
||||||
|
t.Fatalf("resolveAgentRuntimeForUninstall() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindAgentIDByNameRejectsLinkedRuntime(t *testing.T) {
|
||||||
|
home := setMainTestHome(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 := findAgentIDByName("opencode")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "physical directory") {
|
||||||
|
t.Fatalf("findAgentIDByName() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindAgentIDByNameRejectsLinkedConfig(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)
|
||||||
|
}
|
||||||
|
target := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
config := []byte(`{"agent_name":"opencode","agent_id":"agent-1"}`)
|
||||||
|
if err := os.WriteFile(target, config, 0600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(target, filepath.Join(runtimeDirectory, "config.json")); err != nil {
|
||||||
|
t.Skipf("file symbolic links unavailable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := findAgentIDByName("opencode")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "physical file") {
|
||||||
|
t.Fatalf("findAgentIDByName() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,3 +126,18 @@ func RequirePhysicalDirectory(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequirePhysicalFile reports whether a path is an existing physical regular file.
|
||||||
|
func RequirePhysicalFile(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 file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
||||||
|
return false, fmt.Errorf("agent runtime config is not a physical file: %s", path)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -93,3 +93,19 @@ func TestResolveAgentRuntimeDirectoryRejectsSymlinkChild(t *testing.T) {
|
|||||||
t.Fatalf("ResolveAgentRuntimeDirectory() error = %v", err)
|
t.Fatalf("ResolveAgentRuntimeDirectory() error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequirePhysicalFileRejectsSymlink(t *testing.T) {
|
||||||
|
setRuntimeTestHome(t)
|
||||||
|
target := filepath.Join(t.TempDir(), "target.json")
|
||||||
|
if err := os.WriteFile(target, []byte("{}"), 0600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error = %v", err)
|
||||||
|
}
|
||||||
|
link := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
if err := os.Symlink(target, link); err != nil {
|
||||||
|
t.Skipf("file symbolic links unavailable: %v", err)
|
||||||
|
}
|
||||||
|
_, err := RequirePhysicalFile(link)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "physical file") {
|
||||||
|
t.Fatalf("RequirePhysicalFile() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
type installedAgentRuntime struct {
|
||||||
|
agentID string
|
||||||
|
agentName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type runtimeIdentity struct {
|
||||||
|
AgentID string `json:"agent_id"`
|
||||||
|
AgentName string `json:"agent_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func readInstalledAgentRuntime(directoryName string) (*installedAgentRuntime, error) {
|
||||||
|
agentDirectory, err := plugins.ResolveAgentRuntimeDirectory(directoryName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
exists, err := plugins.RequirePhysicalDirectory(agentDirectory)
|
||||||
|
if err != nil || !exists {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(agentDirectory, "config.json")
|
||||||
|
configExists, err := plugins.RequirePhysicalFile(configPath)
|
||||||
|
if err != nil || !configExists {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read agent config %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
var identity runtimeIdentity
|
||||||
|
if err := json.Unmarshal(data, &identity); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse agent config %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
if identity.AgentID == "" || identity.AgentName == "" {
|
||||||
|
return nil, fmt.Errorf("agent config %s has an invalid runtime identity", configPath)
|
||||||
|
}
|
||||||
|
if identity.AgentID != directoryName {
|
||||||
|
return nil, fmt.Errorf("agent config %s crosses its runtime directory", configPath)
|
||||||
|
}
|
||||||
|
if _, err := plugins.ResolveAgentRuntimeDirectory(identity.AgentID); err != nil {
|
||||||
|
return nil, fmt.Errorf("validate agent config %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
return &installedAgentRuntime{
|
||||||
|
agentID: identity.AgentID, agentName: identity.AgentName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverInstalledAgentRuntimes() ([]installedAgentRuntime, error) {
|
||||||
|
runtimeRoot, err := plugins.AgentRuntimeRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
exists, err := plugins.RequirePhysicalDirectory(runtimeRoot)
|
||||||
|
if err != nil || !exists {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(runtimeRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read Elydora runtime root %s: %w", runtimeRoot, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimes := make([]installedAgentRuntime, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Type()&os.ModeSymlink != 0 {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"agent runtime path is not a physical directory: %s",
|
||||||
|
filepath.Join(runtimeRoot, entry.Name()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
runtime, err := readInstalledAgentRuntime(entry.Name())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if runtime != nil {
|
||||||
|
runtimes = append(runtimes, *runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runtimes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAgentIDByName returns one validated runtime identity for an agent name.
|
||||||
|
func findAgentIDByName(agentName string) (string, error) {
|
||||||
|
runtimes, err := discoverInstalledAgentRuntimes()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
foundAgentID := ""
|
||||||
|
for _, runtime := range runtimes {
|
||||||
|
if runtime.agentName != agentName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if foundAgentID != "" {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"multiple %q agent runtimes found; pass --agent-id explicitly",
|
||||||
|
agentName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
foundAgentID = runtime.agentID
|
||||||
|
}
|
||||||
|
return foundAgentID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveAgentRuntimeForUninstall(
|
||||||
|
agentName, explicitAgentID string,
|
||||||
|
) (agentID, agentDirectory string, directoryExists bool, err error) {
|
||||||
|
runtimeRoot, err := plugins.AgentRuntimeRoot()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false, err
|
||||||
|
}
|
||||||
|
runtimeRootExists, err := plugins.RequirePhysicalDirectory(runtimeRoot)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
agentID = explicitAgentID
|
||||||
|
if agentID == "" {
|
||||||
|
agentID, err = findAgentIDByName(agentName)
|
||||||
|
if err != nil || agentID == "" {
|
||||||
|
return agentID, "", false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agentDirectory, err = plugins.ResolveAgentRuntimeDirectory(agentID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false, err
|
||||||
|
}
|
||||||
|
if runtimeRootExists {
|
||||||
|
directoryExists, err = plugins.RequirePhysicalDirectory(agentDirectory)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if directoryExists {
|
||||||
|
runtime, err := readInstalledAgentRuntime(agentID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false, err
|
||||||
|
}
|
||||||
|
if runtime != nil && runtime.agentName != agentName {
|
||||||
|
return "", "", false, fmt.Errorf(
|
||||||
|
"agent runtime %q belongs to %q, not %q",
|
||||||
|
agentID, runtime.agentName, agentName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return agentID, agentDirectory, directoryExists, nil
|
||||||
|
}
|
||||||
Reference in new issue
Block a user