From 1ede03d989842c2c7089d82fe7dc8217acbc9823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 3 Sep 2026 01:56:07 -0400 Subject: [PATCH] chore(go): audit providers copilot droid augment cline Move the four providers onto managed_runtime.go and shell_command.go; keep the Auggie wrappers, Cline metadata wrappers, and the Copilot and Droid precondition handling provider-local. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq --- cmd/elydora/plugins/augment.go | 9 +- cmd/elydora/plugins/augment_contract.go | 64 +--- cmd/elydora/plugins/augment_installation.go | 196 +++-------- cmd/elydora/plugins/augment_io.go | 44 --- cmd/elydora/plugins/augment_status.go | 313 ++---------------- .../plugins/augment_transaction_test.go | 7 +- .../plugins/claudecode_security_test.go | 4 +- cmd/elydora/plugins/cline.go | 3 +- cmd/elydora/plugins/cline_contract.go | 45 +-- cmd/elydora/plugins/cline_installation.go | 154 ++------- cmd/elydora/plugins/cline_io.go | 15 +- cmd/elydora/plugins/cline_status.go | 205 +----------- cmd/elydora/plugins/copilot.go | 8 +- cmd/elydora/plugins/copilot_contract.go | 182 ++-------- cmd/elydora/plugins/copilot_installation.go | 162 ++------- cmd/elydora/plugins/copilot_io.go | 9 +- cmd/elydora/plugins/copilot_status.go | 260 +-------------- cmd/elydora/plugins/cursor_test.go | 2 +- cmd/elydora/plugins/droid.go | 10 +- cmd/elydora/plugins/droid_config.go | 2 +- cmd/elydora/plugins/droid_contract.go | 138 +------- cmd/elydora/plugins/droid_installation.go | 179 ++-------- cmd/elydora/plugins/droid_status.go | 250 +------------- cmd/elydora/plugins/droid_test.go | 2 +- .../plugins/droid_test_helpers_test.go | 4 +- cmd/elydora/plugins/droid_transaction_test.go | 10 +- cmd/elydora/plugins/gemini_security_test.go | 4 +- cmd/elydora/plugins/managed_runtime.go | 4 +- 28 files changed, 234 insertions(+), 2051 deletions(-) diff --git a/cmd/elydora/plugins/augment.go b/cmd/elydora/plugins/augment.go index b09a1f4..cb7ef10 100644 --- a/cmd/elydora/plugins/augment.go +++ b/cmd/elydora/plugins/augment.go @@ -10,8 +10,7 @@ type AugmentPlugin struct { rename renameFunc } -// ManagesGuardRuntime reports that Auggie commits its provider guard together -// with both wrappers, the audit runtime, and user settings. +// ManagesGuardRuntime reports that the guard is committed with the audit runtime. func (p *AugmentPlugin) ManagesGuardRuntime() bool { return true } @@ -57,13 +56,14 @@ func (p *AugmentPlugin) Install(config InstallConfig) error { if err != nil { return err } - if err := writeAugmentChanges( + if err := writeManagedChanges( changes, "Install Augment Code CLI hooks", p.rename, paths.runtimeRoot, paths.agentDirectory, filepath.Dir(document.configPath), + "Auggie configuration directory", ); err != nil { return err } @@ -99,13 +99,14 @@ func (p *AugmentPlugin) Uninstall(agentID string) error { if err != nil { return err } - return writeAugmentChanges( + return writeManagedChanges( []*fileChange{change}, "Uninstall Augment Code CLI hooks", p.rename, "", "", filepath.Dir(document.configPath), + "Auggie configuration directory", ) } diff --git a/cmd/elydora/plugins/augment_contract.go b/cmd/elydora/plugins/augment_contract.go index ddab87f..39088d4 100644 --- a/cmd/elydora/plugins/augment_contract.go +++ b/cmd/elydora/plugins/augment_contract.go @@ -12,11 +12,10 @@ import ( ) const ( - augmentAgentKey = "augment" - augmentGuardScript = "guard.js" - augmentAuditScript = "hook.js" - augmentHookTimeout = float64(10_000) - augmentPOSIXApostrophe = `'"'"'` + augmentAgentKey = "augment" + augmentGuardScript = "guard.js" + augmentAuditScript = "hook.js" + augmentHookTimeout = float64(10_000) ) var ( @@ -314,23 +313,8 @@ func readAugmentWindowsArgument(command string) (string, bool) { } func readAugmentPOSIXArgument(command string) (string, bool) { - if len(command) < 2 || command[0] != '\'' { - return "", false - } - var value strings.Builder - for index := 1; index < len(command); { - if strings.HasPrefix(command[index:], augmentPOSIXApostrophe) { - value.WriteByte('\'') - index += len(augmentPOSIXApostrophe) - continue - } - if command[index] == '\'' { - return value.String(), index == len(command)-1 && value.Len() > 0 - } - value.WriteByte(command[index]) - index++ - } - return "", false + value, end, ok := readPOSIXArgument(command, 0) + return value, ok && end == len(command) && value != "" } func parseAugmentCommand(command string) (string, bool) { @@ -340,36 +324,6 @@ func parseAugmentCommand(command string) (string, bool) { return readAugmentPOSIXArgument(command) } -func normalizeAugmentPath(value string) string { - absolute, err := filepath.Abs(value) - if err == nil { - value = absolute - } - value = filepath.ToSlash(filepath.Clean(value)) - if runtime.GOOS == "windows" { - return strings.ToLower(value) - } - return value -} - -func sameAugmentPath(left, right string) bool { - return normalizeAugmentPath(left) == normalizeAugmentPath(right) -} - -func sameAugmentAgentID(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func sameAugmentFileName(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - func managedAugmentAgentID( handler map[string]any, wrapperName, runtimeRoot string, @@ -385,11 +339,11 @@ func managedAugmentAgentID( return "", false } wrapperPath, ok := parseAugmentCommand(command) - if !ok || !sameAugmentFileName(filepath.Base(wrapperPath), wrapperName) { + if !ok || !sameManagedName(filepath.Base(wrapperPath), wrapperName) { return "", false } agentDirectory := filepath.Dir(wrapperPath) - if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) { + if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) { return "", false } agentID := filepath.Base(agentDirectory) @@ -407,7 +361,7 @@ func removeManagedAugmentGroups( groupChanged := false for _, handler := range group.handlers { managedID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot) - remove := managed && (agentID == "" || sameAugmentAgentID(managedID, agentID)) + remove := managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) if remove { changed = true groupChanged = true diff --git a/cmd/elydora/plugins/augment_installation.go b/cmd/elydora/plugins/augment_installation.go index 43e030a..c35437c 100644 --- a/cmd/elydora/plugins/augment_installation.go +++ b/cmd/elydora/plugins/augment_installation.go @@ -1,96 +1,42 @@ package plugins -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" -) +import "fmt" type augmentRuntimePaths struct { - runtimeRoot string - agentDirectory string - configPath string - keyPath string - guardPath string - auditPath string + managedRuntimePaths guardWrapperPath string auditWrapperPath string } -func validateAugmentInstallConfig(config InstallConfig) error { - for _, field := range []struct{ name, value string }{ - {"agent name", config.AgentName}, - {"organization ID", config.OrgID}, - {"agent ID", config.AgentID}, - {"key ID", config.KID}, - {"private key", config.PrivateKey}, - {"base URL", config.BaseURL}, - {"guard script path", config.GuardScriptPath}, - } { - if field.value == "" { - return fmt.Errorf("%s is required", field.name) - } +func augmentWrapperArtifacts(agentDirectory string) []managedArtifact { + wrappers := resolveAugmentWrapperPaths(agentDirectory) + return []managedArtifact{ + {wrappers.guard, "Auggie guard wrapper", maxManagedSourceBytes}, + {wrappers.audit, "Auggie audit wrapper", maxManagedSourceBytes}, } - if config.AgentName != augmentAgentKey { - return fmt.Errorf( - "Augment Code CLI installation requires agent name %s", - augmentAgentKey, - ) - } - if strings.TrimSpace(config.OrgID) == "" { - return fmt.Errorf("organization ID is required") - } - if strings.TrimSpace(config.KID) == "" { - return fmt.Errorf("key ID is required") - } - if config.Token != "" && strings.TrimSpace(config.Token) == "" { - return fmt.Errorf("token must contain a non-whitespace value when provided") - } - if err := validateManagedPrivateKey(config.PrivateKey); err != nil { - return err - } - return validateManagedBaseURL(config.BaseURL) } func augmentAgentPaths(config InstallConfig) (*augmentRuntimePaths, error) { - if err := validateAugmentInstallConfig(config); err != nil { + if err := validateManagedInstallConfig(config, augmentAgentKey, "Augment Code CLI"); err != nil { return nil, err } - runtimeRoot, err := AgentRuntimeRoot() + paths, err := resolveManagedRuntimePaths(config, augmentGuardScript, augmentAuditScript) if err != nil { return nil, err } - agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID) - if err != nil { - return nil, err - } - wrappers := resolveAugmentWrapperPaths(agentDirectory) - paths := &augmentRuntimePaths{ - runtimeRoot: runtimeRoot, agentDirectory: agentDirectory, - configPath: filepath.Join(agentDirectory, "config.json"), - keyPath: filepath.Join(agentDirectory, "private.key"), - guardPath: filepath.Join(agentDirectory, augmentGuardScript), - auditPath: filepath.Join(agentDirectory, augmentAuditScript), - guardWrapperPath: wrappers.guard, - auditWrapperPath: wrappers.audit, - } - if !filepath.IsAbs(config.GuardScriptPath) || - !sameAugmentPath(config.GuardScriptPath, paths.guardPath) { - return nil, fmt.Errorf( - "Elydora guard runtime must use the managed agent directory: %s", - paths.guardPath, - ) - } - if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) || - !sameAugmentPath(config.HookScript, paths.auditPath)) { - return nil, fmt.Errorf( - "Elydora audit runtime must use the managed agent directory: %s", - paths.auditPath, - ) - } - return paths, nil + wrappers := resolveAugmentWrapperPaths(paths.agentDirectory) + return &augmentRuntimePaths{ + managedRuntimePaths: *paths, + guardWrapperPath: wrappers.guard, + auditWrapperPath: wrappers.audit, + }, nil +} + +func validateAugmentRuntimeIdentity(agentDirectory, agentID string) error { + return validateManagedRuntimeIdentity( + agentDirectory, agentID, augmentAgentKey, "Auggie", + augmentWrapperArtifacts(agentDirectory)..., + ) } func preflightAugmentInstallation( @@ -98,55 +44,25 @@ func preflightAugmentInstallation( document *augmentDocument, ) (*augmentRuntimePaths, string, error) { if document == nil || document.configPath == "" { - return nil, "", fmt.Errorf( - "Augment Code CLI installation requires a user settings document", - ) + return nil, "", fmt.Errorf("Augment Code CLI installation requires a user settings document") } paths, err := augmentAgentPaths(config) if err != nil { return nil, "", err } - if err := validateAugmentRuntimeIdentity( - paths.agentDirectory, - config.AgentID, - ); err != nil { + if err := validateAugmentRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil { return nil, "", err } - nodePath, err := resolveNodeRuntime() + nodePath, err := resolveAbsoluteNodeRuntime("Auggie") if err != nil { return nil, "", err } - if err := requireAugmentAbsoluteNode(nodePath); err != nil { - return nil, "", err - } if err := validateAugmentMatchers(document.hooks, nodePath); err != nil { return nil, "", err } return paths, nodePath, nil } -func buildAugmentRuntimeConfig(config InstallConfig) ([]byte, error) { - value := map[string]any{ - "org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID, - "base_url": config.BaseURL, "agent_name": augmentAgentKey, - } - if config.Token != "" { - value["token"] = config.Token - } - encoded, err := json.MarshalIndent(value, "", " ") - if err != nil { - return nil, fmt.Errorf("encode Elydora runtime config: %w", err) - } - encoded = append(encoded, '\n') - if len(encoded) > maxRuntimeConfigBytes { - return nil, fmt.Errorf( - "Elydora runtime config exceeds %d bytes after JSON encoding", - maxRuntimeConfigBytes, - ) - } - return encoded, nil -} - func prepareAugmentInstallationChanges( config InstallConfig, paths *augmentRuntimePaths, @@ -156,63 +72,27 @@ func prepareAugmentInstallationChanges( if paths == nil || rendered == nil { return nil, fmt.Errorf("prepared Auggie installation is required") } - if err := validateAugmentRuntimeIdentity( - paths.agentDirectory, - config.AgentID, - ); err != nil { + if err := validateAugmentRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil { return nil, err } - runtimeConfig, err := buildAugmentRuntimeConfig(config) + changes, err := managedRuntimeFileChanges( + config, + &paths.managedRuntimePaths, + augmentAgentKey, + generateGuardScript(augmentAgentKey, config.AgentID, "", false, ""), + buildHookScriptWithOutput(augmentAgentKey, config.AgentID, "", false, true), + ) if err != nil { return nil, err } - items := []struct { + for _, item := range []struct { path, label string content []byte - mode os.FileMode }{ - { - paths.guardPath, - "Elydora guard runtime", - []byte(generateGuardScript( - augmentAgentKey, - config.AgentID, - "", - false, - "", - )), - 0700, - }, - {paths.configPath, "Elydora runtime config", runtimeConfig, 0600}, - {paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600}, - { - paths.auditPath, - "Elydora audit runtime", - []byte(buildHookScriptWithOutput( - augmentAgentKey, - config.AgentID, - "", - false, - true, - )), - 0700, - }, - { - paths.guardWrapperPath, - "Auggie guard wrapper", - buildAugmentWrapper(nodePath, paths.guardPath), - 0700, - }, - { - paths.auditWrapperPath, - "Auggie audit wrapper", - buildAugmentWrapper(nodePath, paths.auditPath), - 0700, - }, - } - changes := make([]*fileChange, 0, len(items)+1) - for _, item := range items { - change, err := prepareFileChange(item.path, item.label, item.content, item.mode) + {paths.guardWrapperPath, "Auggie guard wrapper", buildAugmentWrapper(nodePath, paths.guardPath)}, + {paths.auditWrapperPath, "Auggie audit wrapper", buildAugmentWrapper(nodePath, paths.auditPath)}, + } { + change, err := prepareFileChange(item.path, item.label, item.content, 0700) if err != nil { return nil, err } diff --git a/cmd/elydora/plugins/augment_io.go b/cmd/elydora/plugins/augment_io.go index 0d0b90e..a6365b7 100644 --- a/cmd/elydora/plugins/augment_io.go +++ b/cmd/elydora/plugins/augment_io.go @@ -83,43 +83,6 @@ func prepareRenderedAugmentChange( ) } -func writeAugmentChanges( - changes []*fileChange, - label string, - rename renameFunc, - runtimeRoot string, - agentDirectory string, - settingsDirectory string, -) error { - hasChanges := false - for _, change := range changes { - if change != nil { - hasChanges = true - break - } - } - if !hasChanges { - return nil - } - if agentDirectory != "" { - if err := EnsurePrivateDirectory(runtimeRoot); err != nil { - return err - } - if err := EnsurePrivateDirectory(agentDirectory); err != nil { - return err - } - } - if settingsDirectory != "" { - if err := ensureManagedDirectory( - settingsDirectory, - "Auggie configuration directory", - ); err != nil { - return err - } - } - return writeChanges(changes, label, rename) -} - func validateAugmentMatchers(hooks augmentHooks, nodePath string) error { events := make([]string, 0, len(hooks)) for event := range hooks { @@ -209,10 +172,3 @@ func resolveAugmentWrapperPaths(agentDirectory string) augmentWrapperPaths { audit: filepath.Join(agentDirectory, augmentAuditWrapperName()), } } - -func requireAugmentAbsoluteNode(nodePath string) error { - if !filepath.IsAbs(nodePath) || !isNodeExecutable(nodePath) { - return fmt.Errorf("Auggie hooks require an absolute Node.js executable path") - } - return nil -} diff --git a/cmd/elydora/plugins/augment_status.go b/cmd/elydora/plugins/augment_status.go index c99b4cd..3e15636 100644 --- a/cmd/elydora/plugins/augment_status.go +++ b/cmd/elydora/plugins/augment_status.go @@ -2,11 +2,8 @@ package plugins import ( "bytes" - "fmt" "path/filepath" - "runtime" "sort" - "strings" ) func managedAugmentIDs( @@ -17,19 +14,10 @@ func managedAugmentIDs( result := map[string]string{} for _, group := range groups { for _, handler := range group.handlers { - agentID, managed := managedAugmentAgentID( - handler, - wrapperName, - runtimeRoot, - ) - if !managed { - continue + agentID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot) + if managed { + result[managedReferenceKey(agentID)] = agentID } - key := agentID - if runtime.GOOS == "windows" { - key = strings.ToLower(key) - } - result[key] = agentID } } return result @@ -39,16 +27,8 @@ func augmentRuntimeContracts( hooks augmentHooks, runtimeRoot string, ) []augmentRuntimeContract { - guards := managedAugmentIDs( - hooks["PreToolUse"], - augmentGuardWrapperName(), - runtimeRoot, - ) - audits := managedAugmentIDs( - hooks["PostToolUse"], - augmentAuditWrapperName(), - runtimeRoot, - ) + guards := managedAugmentIDs(hooks["PreToolUse"], augmentGuardWrapperName(), runtimeRoot) + audits := managedAugmentIDs(hooks["PostToolUse"], augmentAuditWrapperName(), runtimeRoot) keys := make([]string, 0, len(guards)) for key := range guards { if _, exists := audits[key]; exists { @@ -72,282 +52,47 @@ func augmentRuntimeContracts( return contracts } -func readAugmentRuntimeConfig(path string) (map[string]any, bool, error) { - snapshot, err := readManagedFile( - path, - "Elydora runtime config", - maxRuntimeConfigBytes, - ) - if err != nil || snapshot == nil { - return nil, snapshot != nil, err +func augmentWrappersMatch(contract augmentRuntimeContract, nodePath string) (bool, error) { + wrappers := resolveAugmentWrapperPaths(filepath.Dir(contract.guardPath)) + if !sameManagedPath(contract.guardWrapper, wrappers.guard) || + !sameManagedPath(contract.auditWrapper, wrappers.audit) { + return false, nil } - label := fmt.Sprintf("Elydora runtime config at %s", path) - config, err := decodeStrictJSONObject(snapshot.contents, label) - return config, true, err -} - -func requireAugmentRuntimeString( - config map[string]any, - field string, - configPath string, -) (string, error) { - value, ok := config[field].(string) - if !ok || strings.TrimSpace(value) == "" { - return "", fmt.Errorf( - "Elydora runtime config %s is invalid: %s", - field, - configPath, - ) - } - return value, nil -} - -func validateAugmentRuntimeConfig( - config map[string]any, - expectedAgentID string, - configPath string, -) error { - supported := stringSet( - "org_id", - "agent_id", - "kid", - "base_url", - "token", - "agent_name", - ) - fields := make([]string, 0, len(config)) - for field := range config { - fields = append(fields, field) - } - sort.Strings(fields) - for _, field := range fields { - if _, ok := supported[field]; !ok { - return fmt.Errorf( - `Elydora runtime config has unsupported field %q: %s`, - field, - configPath, - ) - } - } - if _, err := requireAugmentRuntimeString(config, "org_id", configPath); err != nil { - return err - } - if _, err := requireAugmentRuntimeString(config, "kid", configPath); err != nil { - return err - } - agentID, err := requireAugmentRuntimeString(config, "agent_id", configPath) - if err != nil { - return err - } - if !sameAugmentAgentID(agentID, expectedAgentID) || - config["agent_name"] != augmentAgentKey { - return fmt.Errorf( - "Elydora runtime identity does not match Auggie hooks: %s", - configPath, - ) - } - if _, exists := config["token"]; exists { - if _, err := requireAugmentRuntimeString(config, "token", configPath); err != nil { - return err - } - } - baseURL, err := requireAugmentRuntimeString(config, "base_url", configPath) - if err != nil { - return err - } - if err := validateManagedBaseURL(baseURL); err != nil { - return fmt.Errorf( - "Elydora runtime config base URL is invalid at %s: %w", - configPath, - err, - ) - } - return nil -} - -func validateAugmentRuntimeIdentity(agentDirectory, agentID string) error { - runtimeRoot := filepath.Dir(agentDirectory) - rootExists, err := managedPhysicalDirectoryExists( - runtimeRoot, - "Elydora runtime directory", - ) - if err != nil || !rootExists { - return err - } - directoryExists, err := managedPhysicalDirectoryExists( - agentDirectory, - "Elydora agent runtime directory", - ) - if err != nil || !directoryExists { - return err - } - configPath := filepath.Join(agentDirectory, "config.json") - config, configExists, err := readAugmentRuntimeConfig(configPath) - if err != nil { - return err - } - artifactExists := false for _, item := range []struct { - path string - label string - limit int64 + path, label string + expected []byte }{ - {filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes}, - {filepath.Join(agentDirectory, augmentGuardScript), "Elydora guard runtime", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, augmentAuditScript), "Elydora audit runtime", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, augmentGuardWrapperName()), "Auggie guard wrapper", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, augmentAuditWrapperName()), "Auggie audit wrapper", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes}, - {filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes}, - {filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes}, + {contract.guardWrapper, "Auggie guard wrapper", buildAugmentWrapper(nodePath, contract.guardPath)}, + {contract.auditWrapper, "Auggie audit wrapper", buildAugmentWrapper(nodePath, contract.auditPath)}, } { - exists, inspectErr := managedPhysicalFileExists( - item.path, - item.label, - item.limit, - ) - if inspectErr != nil { - return inspectErr + snapshot, err := readManagedFile(item.path, item.label, maxManagedSourceBytes) + if err != nil || snapshot == nil || !bytes.Equal(snapshot.contents, item.expected) { + return false, err } - artifactExists = artifactExists || exists } - if !configExists { - if artifactExists { - return fmt.Errorf( - "Elydora runtime identity cannot be verified without config.json: %s", - agentDirectory, - ) - } - return nil - } - configuredID, ok := config["agent_id"].(string) - if !ok || config["agent_name"] != augmentAgentKey || - !sameAugmentAgentID(configuredID, agentID) { - return fmt.Errorf( - "Elydora runtime config identity does not match Auggie agent %s: %s", - agentID, - configPath, - ) - } - return nil -} - -func augmentRuntimeContractExists( - contract augmentRuntimeContract, - runtimeRoot string, - nodePath string, -) (bool, error) { - agentDirectory := filepath.Dir(contract.guardPath) - wrappers := resolveAugmentWrapperPaths(agentDirectory) - if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) || - !sameAugmentPath( - contract.auditPath, - filepath.Join(agentDirectory, augmentAuditScript), - ) || - !sameAugmentPath(contract.guardWrapper, wrappers.guard) || - !sameAugmentPath(contract.auditWrapper, wrappers.audit) { - return false, nil - } - rootExists, err := managedPhysicalDirectoryExists( - runtimeRoot, - "Elydora runtime directory", - ) - if err != nil || !rootExists { - return false, err - } - directoryExists, err := managedPhysicalDirectoryExists( - agentDirectory, - "Elydora agent runtime directory", - ) - if err != nil || !directoryExists { - return false, err - } - configPath := filepath.Join(agentDirectory, "config.json") - keyPath := filepath.Join(agentDirectory, "private.key") - config, configExists, err := readAugmentRuntimeConfig(configPath) - if err != nil { - return false, err - } - key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes) - if err != nil { - return false, err - } - guard, err := readManagedFile( - contract.guardPath, - "Elydora guard runtime", - maxManagedSourceBytes, - ) - if err != nil { - return false, err - } - audit, err := readManagedFile( - contract.auditPath, - "Elydora audit runtime", - maxManagedSourceBytes, - ) - if err != nil { - return false, err - } - guardWrapper, err := readManagedFile( - contract.guardWrapper, - "Auggie guard wrapper", - maxManagedSourceBytes, - ) - if err != nil { - return false, err - } - auditWrapper, err := readManagedFile( - contract.auditWrapper, - "Auggie audit wrapper", - maxManagedSourceBytes, - ) - if err != nil { - return false, err - } - if !configExists || key == nil || guard == nil || audit == nil || - guardWrapper == nil || auditWrapper == nil { - return false, nil - } - if err := validateAugmentRuntimeConfig( - config, - contract.agentID, - configPath, - ); err != nil { - return false, err - } - if err := validateManagedPrivateKey(string(key.contents)); err != nil { - return false, fmt.Errorf("Elydora private key at %s: %w", keyPath, err) - } - return len(guard.contents) > 0 && - len(audit.contents) > 0 && - bytes.Equal( - guardWrapper.contents, - buildAugmentWrapper(nodePath, contract.guardPath), - ) && - bytes.Equal( - auditWrapper.contents, - buildAugmentWrapper(nodePath, contract.auditPath), - ), nil + return true, nil } func augmentRuntimeFilesExist( contracts []augmentRuntimeContract, runtimeRoot string, ) (bool, error) { - nodePath, err := resolveNodeRuntime() + nodePath, err := resolveAbsoluteNodeRuntime("Auggie") if err != nil { return false, err } - if err := requireAugmentAbsoluteNode(nodePath); err != nil { - return false, err - } for _, contract := range contracts { - exists, err := augmentRuntimeContractExists(contract, runtimeRoot, nodePath) - if err != nil { - return false, err + runtime := managedRuntimeContract{contract.agentID, contract.guardPath, contract.auditPath} + exists, err := managedRuntimeContractExists(runtime, augmentAgentKey, "Auggie", nil) + if err != nil || !exists { + if err != nil { + return false, err + } + continue } - if exists { - return true, nil + matches, err := augmentWrappersMatch(contract, nodePath) + if err != nil || matches { + return matches, err } } return false, nil diff --git a/cmd/elydora/plugins/augment_transaction_test.go b/cmd/elydora/plugins/augment_transaction_test.go index 85daf7d..09559c2 100644 --- a/cmd/elydora/plugins/augment_transaction_test.go +++ b/cmd/elydora/plugins/augment_transaction_test.go @@ -54,7 +54,7 @@ func TestAugmentInstallRollsBackAllSevenFiles(t *testing.T) { ) failed := false fixture.plugin.rename = func(source, destination string) error { - if sameAugmentPath(destination, fixture.configPath) && + if sameManagedPath(destination, fixture.configPath) && strings.HasSuffix(source, ".tmp") && !failed { failed = true @@ -87,13 +87,14 @@ func TestPreparedAugmentInstallRejectsConcurrentSettingsChange(t *testing.T) { if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil { t.Fatalf("write concurrent settings: %v", err) } - err := writeAugmentChanges( + err := writeManagedChanges( changes, "Install Augment Code CLI hooks", nil, paths.runtimeRoot, paths.agentDirectory, filepath.Dir(document.configPath), + "Auggie configuration directory", ) if err == nil || !strings.Contains(err.Error(), "changed during installation") { t.Fatalf("prepared install error = %v", err) @@ -375,7 +376,7 @@ func TestAugmentUninstallPreservesSettingsAfterCommitFailure(t *testing.T) { t.Fatalf("read installed settings: %v", err) } fixture.plugin.rename = func(source, destination string) error { - if sameAugmentPath(destination, fixture.configPath) && + if sameManagedPath(destination, fixture.configPath) && strings.HasSuffix(source, ".tmp") { return errors.New("injected Auggie uninstall failure") } diff --git a/cmd/elydora/plugins/claudecode_security_test.go b/cmd/elydora/plugins/claudecode_security_test.go index 3753f24..f248e7d 100644 --- a/cmd/elydora/plugins/claudecode_security_test.go +++ b/cmd/elydora/plugins/claudecode_security_test.go @@ -150,10 +150,10 @@ func TestClaudeValidatesManagedRuntimeInputsBeforeWrites(t *testing.T) { }, "query parameters"}, {"guard path", func(f *claudeFixture) { f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js") - }, "managed Elydora agent directory"}, + }, "managed agent directory"}, {"audit path", func(f *claudeFixture) { f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js") - }, "managed Elydora agent directory"}, + }, "managed agent directory"}, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { diff --git a/cmd/elydora/plugins/cline.go b/cmd/elydora/plugins/cline.go index 0db5396..acfbb28 100644 --- a/cmd/elydora/plugins/cline.go +++ b/cmd/elydora/plugins/cline.go @@ -7,8 +7,7 @@ type ClinePlugin struct { rename renameFunc } -// ManagesGuardRuntime reports that Cline commits both generated runtimes and -// both native hook files in one transaction. +// ManagesGuardRuntime reports that the guard is committed with the audit runtime. func (p *ClinePlugin) ManagesGuardRuntime() bool { return true } diff --git a/cmd/elydora/plugins/cline_contract.go b/cmd/elydora/plugins/cline_contract.go index 5d2f0d1..5cdde35 100644 --- a/cmd/elydora/plugins/cline_contract.go +++ b/cmd/elydora/plugins/cline_contract.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "runtime" "strings" ) @@ -80,37 +79,6 @@ func resolveClineHookFiles() (clineHookPaths, error) { }, nil } -func sameClineAgentID(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func normalizeClinePath(value string) (string, error) { - absolute, err := filepath.Abs(value) - if err != nil { - return "", err - } - normalized := filepath.Clean(absolute) - if runtime.GOOS == "windows" { - normalized = strings.ToLower(normalized) - } - return normalized, nil -} - -func sameClinePath(left, right string) (bool, error) { - normalizedLeft, err := normalizeClinePath(left) - if err != nil { - return false, err - } - normalizedRight, err := normalizeClinePath(right) - if err != nil { - return false, err - } - return normalizedLeft == normalizedRight, nil -} - func buildClineMetadata(kind, agentID, runtimePath string) (clineHookMetadata, error) { metadata := clineHookMetadata{ Version: clineMetadataVersion, @@ -314,7 +282,7 @@ func clineContractForFiles( if guard.Kind != "guard" || audit.Kind != "audit" { return nil, fmt.Errorf("Elydora Cline hook files contain mismatched event metadata") } - if !sameClineAgentID(guard.AgentID, audit.AgentID) { + if !sameManagedAgentID(guard.AgentID, audit.AgentID) { return nil, fmt.Errorf("Elydora Cline hook files reference different agents") } agentDirectory, err := ResolveAgentRuntimeDirectory(guard.AgentID) @@ -323,15 +291,8 @@ func clineContractForFiles( } expectedGuard := filepath.Join(agentDirectory, clineGuardScript) expectedAudit := filepath.Join(agentDirectory, clineAuditScript) - guardMatches, err := sameClinePath(guard.RuntimePath, expectedGuard) - if err != nil { - return nil, fmt.Errorf("compare Cline guard runtime path: %w", err) - } - auditMatches, err := sameClinePath(audit.RuntimePath, expectedAudit) - if err != nil { - return nil, fmt.Errorf("compare Cline audit runtime path: %w", err) - } - if !guardMatches || !auditMatches { + if !sameManagedPath(guard.RuntimePath, expectedGuard) || + !sameManagedPath(audit.RuntimePath, expectedAudit) { return nil, fmt.Errorf("Elydora Cline hook metadata references an unexpected runtime path") } return &clineRuntimeContract{ diff --git a/cmd/elydora/plugins/cline_installation.go b/cmd/elydora/plugins/cline_installation.go index 354dff0..d6c3056 100644 --- a/cmd/elydora/plugins/cline_installation.go +++ b/cmd/elydora/plugins/cline_installation.go @@ -1,97 +1,21 @@ package plugins -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" -) +import "fmt" type clineRuntimePaths struct { - runtimeRoot string - agentDirectory string - configPath string - keyPath string - guardPath string - auditPath string - hooks clineHookPaths + managedRuntimePaths + hooks clineHookPaths } -func validateClineInstallConfig(config InstallConfig) error { - for _, field := range []struct{ name, value string }{ - {"agent name", config.AgentName}, - {"organization ID", config.OrgID}, - {"agent ID", config.AgentID}, - {"key ID", config.KID}, - {"private key", config.PrivateKey}, - {"base URL", config.BaseURL}, - {"guard script path", config.GuardScriptPath}, - } { - if field.value == "" { - return fmt.Errorf("%s is required", field.name) - } - } - if config.AgentName != clineAgentKey { - return fmt.Errorf("Cline installation requires agent name %s", clineAgentKey) - } - if strings.TrimSpace(config.OrgID) == "" { - return fmt.Errorf("organization ID is required") - } - if strings.TrimSpace(config.KID) == "" { - return fmt.Errorf("key ID is required") - } - if config.Token != "" && strings.TrimSpace(config.Token) == "" { - return fmt.Errorf("token must contain a non-whitespace value when provided") - } - if err := validateManagedPrivateKey(config.PrivateKey); err != nil { - return err - } - return validateManagedBaseURL(config.BaseURL) -} - -func clineAgentPaths( - config InstallConfig, - hooks clineHookPaths, -) (*clineRuntimePaths, error) { - if err := validateClineInstallConfig(config); err != nil { +func clineAgentPaths(config InstallConfig, hooks clineHookPaths) (*clineRuntimePaths, error) { + if err := validateManagedInstallConfig(config, clineAgentKey, "Cline"); err != nil { return nil, err } - runtimeRoot, err := AgentRuntimeRoot() + paths, err := resolveManagedRuntimePaths(config, clineGuardScript, clineAuditScript) if err != nil { return nil, err } - agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID) - if err != nil { - return nil, err - } - paths := &clineRuntimePaths{ - runtimeRoot: runtimeRoot, agentDirectory: agentDirectory, hooks: hooks, - configPath: filepath.Join(agentDirectory, "config.json"), - keyPath: filepath.Join(agentDirectory, "private.key"), - guardPath: filepath.Join(agentDirectory, clineGuardScript), - auditPath: filepath.Join(agentDirectory, clineAuditScript), - } - if !filepath.IsAbs(config.GuardScriptPath) || - !sameClinePathValue(config.GuardScriptPath, paths.guardPath) { - return nil, fmt.Errorf( - "Elydora guard runtime must use the managed agent directory: %s", - paths.guardPath, - ) - } - if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) || - !sameClinePathValue(config.HookScript, paths.auditPath)) { - return nil, fmt.Errorf( - "Elydora audit runtime must use the managed agent directory: %s", - paths.auditPath, - ) - } - return paths, nil -} - -func sameClinePathValue(left, right string) bool { - matches, err := sameClinePath(left, right) - return err == nil && matches + return &clineRuntimePaths{managedRuntimePaths: *paths, hooks: hooks}, nil } func preflightClineInstallation( @@ -102,39 +26,20 @@ func preflightClineInstallation( if err != nil { return nil, err } - if err := validateClineRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil { + if err := validateManagedRuntimeIdentity( + paths.agentDirectory, config.AgentID, clineAgentKey, "Cline", + ); err != nil { return nil, err } - nodePath, err := resolveNodeRuntime() - if err != nil { + if _, err := resolveAbsoluteNodeRuntime("Cline"); err != nil { return nil, err } - if !filepath.IsAbs(nodePath) || !isNodeExecutable(nodePath) { - return nil, fmt.Errorf("Cline hooks require an absolute Node.js executable path") - } return paths, nil } -func buildClineRuntimeConfig(config InstallConfig) ([]byte, error) { - value := map[string]any{ - "org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID, - "base_url": config.BaseURL, "agent_name": clineAgentKey, - } - if config.Token != "" { - value["token"] = config.Token - } - encoded, err := json.MarshalIndent(value, "", " ") - if err != nil { - return nil, fmt.Errorf("encode Elydora runtime config: %w", err) - } - encoded = append(encoded, '\n') - if len(encoded) > maxRuntimeConfigBytes { - return nil, fmt.Errorf( - "Elydora runtime config exceeds %d bytes after JSON encoding", - maxRuntimeConfigBytes, - ) - } - return encoded, nil +func clineExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) { + return []byte(generateGuardScript(clineAgentKey, agentID, "", false, "")), + []byte(buildHookScriptWithOutput(clineAgentKey, agentID, "", false, true)) } func prepareClineInstallationChanges( @@ -146,11 +51,9 @@ func prepareClineInstallationChanges( if paths == nil { return nil, fmt.Errorf("prepared Cline installation is required") } - if err := validateClineRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil { - return nil, err - } - runtimeConfig, err := buildClineRuntimeConfig(config) - if err != nil { + if err := validateManagedRuntimeIdentity( + paths.agentDirectory, config.AgentID, clineAgentKey, "Cline", + ); err != nil { return nil, err } guardMetadata, err := buildClineMetadata("guard", config.AgentID, paths.guardPath) @@ -175,23 +78,12 @@ func prepareClineInstallationChanges( ); err != nil { return nil, err } - runtimeItems := []struct { - path, label string - content []byte - mode os.FileMode - }{ - {paths.guardPath, "Elydora guard runtime", []byte(generateGuardScript(clineAgentKey, config.AgentID, "", false, "")), 0700}, - {paths.configPath, "Elydora runtime config", runtimeConfig, 0600}, - {paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600}, - {paths.auditPath, "Elydora audit runtime", []byte(buildHookScriptWithOutput(clineAgentKey, config.AgentID, "", false, true)), 0700}, - } - changes := make([]*fileChange, 0, len(runtimeItems)+2) - for _, item := range runtimeItems { - change, err := prepareFileChange(item.path, item.label, item.content, item.mode) - if err != nil { - return nil, err - } - changes = append(changes, change) + guardScript, auditScript := clineExpectedScripts(config.AgentID, nil) + changes, err := managedRuntimeFileChanges( + config, &paths.managedRuntimePaths, clineAgentKey, string(guardScript), string(auditScript), + ) + if err != nil { + return nil, err } for _, item := range []struct { file clineHookFile diff --git a/cmd/elydora/plugins/cline_io.go b/cmd/elydora/plugins/cline_io.go index bc8d549..522702d 100644 --- a/cmd/elydora/plugins/cline_io.go +++ b/cmd/elydora/plugins/cline_io.go @@ -74,7 +74,7 @@ func writeClineHookChanges( rename renameFunc, paths clineHookPaths, ) error { - if !hasClineChanges(changes) { + if !hasFileChanges(changes) { return nil } if err := ensureManagedDirectory( @@ -95,7 +95,7 @@ func writeClineChanges( rename renameFunc, paths *clineRuntimePaths, ) error { - if !hasClineChanges(changes) { + if !hasFileChanges(changes) { return nil } if err := EnsurePrivateDirectory(paths.runtimeRoot); err != nil { @@ -107,15 +107,6 @@ func writeClineChanges( return writeClineHookChanges(changes, label, rename, paths.hooks) } -func hasClineChanges(changes []*fileChange) bool { - for _, change := range changes { - if change != nil { - return true - } - } - return false -} - func prepareClineUninstallChanges( files []clineHookFile, agentID string, @@ -123,7 +114,7 @@ func prepareClineUninstallChanges( changes := make([]*fileChange, 0, len(files)) for _, file := range files { if file.metadata == nil || - (agentID != "" && !sameClineAgentID(file.metadata.AgentID, agentID)) { + (agentID != "" && !sameManagedAgentID(file.metadata.AgentID, agentID)) { continue } if err := assertClineWrapperIntegrity(file); err != nil { diff --git a/cmd/elydora/plugins/cline_status.go b/cmd/elydora/plugins/cline_status.go index daeef04..5ec6896 100644 --- a/cmd/elydora/plugins/cline_status.go +++ b/cmd/elydora/plugins/cline_status.go @@ -1,206 +1,15 @@ package plugins -import ( - "bytes" - "fmt" - "path/filepath" - "sort" - "strings" -) - -func readClineRuntimeConfig(path string) (map[string]any, bool, error) { - snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes) - if err != nil || snapshot == nil { - return nil, snapshot != nil, err - } - label := fmt.Sprintf("Elydora runtime config at %s", path) - config, err := decodeStrictJSONObject(snapshot.contents, label) - return config, true, err -} - -func requireClineRuntimeString( - config map[string]any, - field string, - configPath string, -) (string, error) { - value, ok := config[field].(string) - if !ok || strings.TrimSpace(value) == "" { - return "", fmt.Errorf( - "Elydora runtime config %s is invalid: %s", - field, - configPath, - ) - } - return value, nil -} - -func validateClineRuntimeConfig( - config map[string]any, - expectedAgentID string, - configPath string, -) error { - supported := stringSet( - "org_id", "agent_id", "kid", "base_url", "token", "agent_name", - ) - fields := make([]string, 0, len(config)) - for field := range config { - fields = append(fields, field) - } - sort.Strings(fields) - for _, field := range fields { - if _, ok := supported[field]; !ok { - return fmt.Errorf( - `Elydora runtime config has unsupported field %q: %s`, - field, - configPath, - ) - } - } - if _, err := requireClineRuntimeString(config, "org_id", configPath); err != nil { - return err - } - if _, err := requireClineRuntimeString(config, "kid", configPath); err != nil { - return err - } - agentID, err := requireClineRuntimeString(config, "agent_id", configPath) - if err != nil { - return err - } - if !sameClineAgentID(agentID, expectedAgentID) || config["agent_name"] != clineAgentKey { - return fmt.Errorf( - "Elydora runtime identity does not match Cline hooks: %s", - configPath, - ) - } - if _, exists := config["token"]; exists { - if _, err := requireClineRuntimeString(config, "token", configPath); err != nil { - return err - } - } - baseURL, err := requireClineRuntimeString(config, "base_url", configPath) - if err != nil { - return err - } - if err := validateManagedBaseURL(baseURL); err != nil { - return fmt.Errorf( - "Elydora runtime config base URL is invalid at %s: %w", - configPath, - err, - ) - } - return nil -} - -func validateClineRuntimeIdentity(agentDirectory, agentID string) error { - runtimeRoot := filepath.Dir(agentDirectory) - rootExists, err := managedPhysicalDirectoryExists( - runtimeRoot, - "Elydora runtime directory", - ) - if err != nil || !rootExists { - return err - } - directoryExists, err := managedPhysicalDirectoryExists( - agentDirectory, - "Elydora agent runtime directory", - ) - if err != nil || !directoryExists { - return err - } - configPath := filepath.Join(agentDirectory, "config.json") - config, configExists, err := readClineRuntimeConfig(configPath) - if err != nil { - return err - } - artifactExists := false - for _, item := range []struct { - path string - label string - limit int64 - }{ - {filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes}, - {filepath.Join(agentDirectory, clineGuardScript), "Elydora guard runtime", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, clineAuditScript), "Elydora audit runtime", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes}, - {filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes}, - {filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes}, - } { - exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit) - if inspectErr != nil { - return inspectErr - } - artifactExists = artifactExists || exists - } - if !configExists { - if artifactExists { - return fmt.Errorf( - "Elydora runtime identity cannot be verified without config.json: %s", - agentDirectory, - ) - } - return nil - } - configuredID, ok := config["agent_id"].(string) - if !ok || config["agent_name"] != clineAgentKey || - !sameClineAgentID(configuredID, agentID) { - return fmt.Errorf( - "Elydora runtime config identity does not match Cline agent %s: %s", - agentID, - configPath, - ) - } - return nil -} +import "path/filepath" func clineRuntimeFilesExist(contract *clineRuntimeContract) (bool, error) { - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return false, err - } - if !sameClinePathValue(filepath.Dir(contract.agentDirectory), runtimeRoot) || - !sameClinePathValue(contract.guardPath, filepath.Join(contract.agentDirectory, clineGuardScript)) || - !sameClinePathValue(contract.auditPath, filepath.Join(contract.agentDirectory, clineAuditScript)) { + if !sameManagedPath(contract.guardPath, filepath.Join(contract.agentDirectory, clineGuardScript)) { return false, nil } - rootExists, err := managedPhysicalDirectoryExists(runtimeRoot, "Elydora runtime directory") - if err != nil || !rootExists { - return false, err - } - directoryExists, err := managedPhysicalDirectoryExists( - contract.agentDirectory, - "Elydora agent runtime directory", + return managedRuntimeContractExists( + managedRuntimeContract{contract.agentID, contract.guardPath, contract.auditPath}, + clineAgentKey, + "Cline", + clineExpectedScripts, ) - if err != nil || !directoryExists { - return false, err - } - configPath := filepath.Join(contract.agentDirectory, "config.json") - keyPath := filepath.Join(contract.agentDirectory, "private.key") - config, configExists, err := readClineRuntimeConfig(configPath) - if err != nil { - return false, err - } - key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes) - if err != nil { - return false, err - } - guard, err := readManagedFile(contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes) - if err != nil { - return false, err - } - audit, err := readManagedFile(contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes) - if err != nil { - return false, err - } - if !configExists || key == nil || guard == nil || audit == nil { - return false, nil - } - if err := validateClineRuntimeConfig(config, contract.agentID, configPath); err != nil { - return false, err - } - if err := validateManagedPrivateKey(string(key.contents)); err != nil { - return false, fmt.Errorf("Elydora private key at %s: %w", keyPath, err) - } - expectedGuard := []byte(generateGuardScript(clineAgentKey, contract.agentID, "", false, "")) - expectedAudit := []byte(buildHookScriptWithOutput(clineAgentKey, contract.agentID, "", false, true)) - return bytes.Equal(guard.contents, expectedGuard) && bytes.Equal(audit.contents, expectedAudit), nil } diff --git a/cmd/elydora/plugins/copilot.go b/cmd/elydora/plugins/copilot.go index 44a3bb0..ef90ccb 100644 --- a/cmd/elydora/plugins/copilot.go +++ b/cmd/elydora/plugins/copilot.go @@ -10,14 +10,12 @@ type CopilotPlugin struct { rename renameFunc } -// ManagesGuardRuntime reports that Copilot commits its provider guard with the -// audit runtime and hook documents. +// ManagesGuardRuntime reports that the guard is committed with the audit runtime. func (p *CopilotPlugin) ManagesGuardRuntime() bool { return true } -// PreflightInstall validates hook sources, settings, matchers, credentials, -// and runtime identity before any write. +// PreflightInstall validates every source before the CLI creates runtime state. func (p *CopilotPlugin) PreflightInstall(config InstallConfig) error { sources, _, err := readCopilotSources() if err != nil { @@ -165,7 +163,7 @@ func mergedCopilotContracts( } unique := map[string]copilotRuntimeContract{} for _, contract := range contracts { - unique[copilotEntryKey(contract.agentID)] = contract + unique[managedReferenceKey(contract.agentID)] = contract } result := make([]copilotRuntimeContract, 0, len(unique)) for _, contract := range unique { diff --git a/cmd/elydora/plugins/copilot_contract.go b/cmd/elydora/plugins/copilot_contract.go index fa6a74a..f8e30f9 100644 --- a/cmd/elydora/plugins/copilot_contract.go +++ b/cmd/elydora/plugins/copilot_contract.go @@ -5,19 +5,17 @@ import ( "encoding/json" "fmt" "path/filepath" - "runtime" "sort" "strings" ) const ( - copilotAgentKey = "copilot" - copilotGuardScript = "guard.js" - copilotAuditScript = "hook.js" - copilotConfigFile = "elydora-audit.json" - copilotHookTimeout = float64(10) - copilotLegacyTimeout = float64(5) - copilotPOSIXApostrophe = `'"'"'` + copilotAgentKey = "copilot" + copilotGuardScript = "guard.js" + copilotAuditScript = "hook.js" + copilotConfigFile = "elydora-audit.json" + copilotHookTimeout = float64(10) + copilotLegacyTimeout = float64(5) ) var copilotManagedEvents = []struct { @@ -61,15 +59,19 @@ type copilotRenderedDocument struct { remove bool } -type copilotRuntimeContract struct { - agentID string - guardPath string - auditPath string -} +type copilotRuntimeContract = managedRuntimeContract -type copilotManagedEntry struct { - agentID string - scriptPath string +type copilotManagedEntry = managedScriptReference + +func copilotManagedEntryForHandler( + handler map[string]any, + scriptName string, +) (*copilotManagedEntry, error) { + scriptPath, managed := copilotManagedScriptPath(handler) + if !managed { + return nil, nil + } + return resolveManagedScript(scriptPath, scriptName) } func cloneCopilotObject(value map[string]any) map[string]any { @@ -90,109 +92,15 @@ func cloneCopilotHooks(source copilotHooks) copilotHooks { return clone } -func sameCopilotPath(left, right string) bool { - left = filepath.Clean(left) - right = filepath.Clean(right) - if absolute, err := filepath.Abs(left); err == nil { - left = absolute - } - if absolute, err := filepath.Abs(right); err == nil { - right = absolute - } - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func sameCopilotAgentID(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func sameCopilotFileName(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func quoteCopilotPowerShell(value string) string { - return "'" + strings.ReplaceAll(value, "'", "''") + "'" -} - func buildCopilotHandler(nodePath, scriptPath string) map[string]any { return map[string]any{ "type": "command", - "bash": quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath), - "powershell": "& " + quoteCopilotPowerShell(nodePath) + " " + quoteCopilotPowerShell(scriptPath) + "; exit $LASTEXITCODE", + "bash": posixSource(nodePath, scriptPath), + "powershell": powerShellSource(nodePath, scriptPath), "timeoutSec": copilotHookTimeout, } } -func readCopilotPOSIXArgument(command string, start int) (string, int, bool) { - if start >= len(command) || command[start] != '\'' { - return "", start, false - } - var value strings.Builder - for index := start + 1; index < len(command); { - if strings.HasPrefix(command[index:], copilotPOSIXApostrophe) { - value.WriteByte('\'') - index += len(copilotPOSIXApostrophe) - continue - } - if command[index] == '\'' { - return value.String(), index + 1, true - } - value.WriteByte(command[index]) - index++ - } - return "", start, false -} - -func readCopilotPowerShellArgument(command string, start int) (string, int, bool) { - if start >= len(command) || command[start] != '\'' { - return "", start, false - } - var value strings.Builder - for index := start + 1; index < len(command); index++ { - if command[index] != '\'' { - value.WriteByte(command[index]) - continue - } - if index+1 < len(command) && command[index+1] == '\'' { - value.WriteByte('\'') - index++ - continue - } - return value.String(), index + 1, true - } - return "", start, false -} - -func parseCopilotBash(command string) (string, string, bool) { - executable, next, ok := readCopilotPOSIXArgument(command, 0) - if !ok || next >= len(command) || command[next] != ' ' { - return "", "", false - } - script, end, ok := readCopilotPOSIXArgument(command, next+1) - return executable, script, ok && end == len(command) -} - -func parseCopilotPowerShell(command string) (string, string, bool) { - if !strings.HasPrefix(command, "& ") { - return "", "", false - } - executable, next, ok := readCopilotPowerShellArgument(command, 2) - if !ok || next >= len(command) || command[next] != ' ' { - return "", "", false - } - script, end, ok := readCopilotPowerShellArgument(command, next+1) - return executable, script, ok && command[end:] == "; exit $LASTEXITCODE" -} - func parseCopilotLegacyCommand(command string) (string, bool) { command = strings.TrimSpace(command) separator := strings.IndexAny(command, " \t") @@ -211,11 +119,6 @@ func parseCopilotLegacyCommand(command string) (string, bool) { return script, script != "" } -func isCopilotNodeExecutable(path string) bool { - name := filepath.Base(path) - return name == "node" || strings.EqualFold(name, "node.exe") -} - func copilotManagedScriptPath(handler map[string]any) (string, bool) { if len(handler) != 4 || handler["type"] != "command" { return "", false @@ -226,11 +129,11 @@ func copilotManagedScriptPath(handler map[string]any) (string, bool) { return "", false } if handler["timeoutSec"] == copilotHookTimeout { - bashNode, bashScript, bashParsed := parseCopilotBash(bash) - psNode, psScript, psParsed := parseCopilotPowerShell(powershell) + bashNode, bashScript, bashParsed := parsePOSIXCommand(bash) + psNode, psScript, psParsed := parsePowerShellSource(powershell) if bashParsed && psParsed && filepath.IsAbs(bashNode) && filepath.IsAbs(bashScript) && - isCopilotNodeExecutable(bashNode) && sameCopilotPath(bashNode, psNode) && - sameCopilotPath(bashScript, psScript) { + isNodeExecutable(bashNode) && sameManagedPath(bashNode, psNode) && + sameManagedPath(bashScript, psScript) { return bashScript, true } } @@ -240,30 +143,6 @@ func copilotManagedScriptPath(handler map[string]any) (string, bool) { return parseCopilotLegacyCommand(bash) } -func copilotManagedEntryForHandler( - handler map[string]any, - scriptName string, -) (*copilotManagedEntry, error) { - scriptPath, managed := copilotManagedScriptPath(handler) - if !managed || !filepath.IsAbs(scriptPath) || - !sameCopilotFileName(filepath.Base(scriptPath), scriptName) { - return nil, nil - } - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return nil, err - } - agentDirectory := filepath.Dir(scriptPath) - if !sameCopilotPath(filepath.Dir(agentDirectory), runtimeRoot) { - return nil, nil - } - agentID := filepath.Base(agentDirectory) - if agentID == "" || agentID == "." || agentID == ".." { - return nil, nil - } - return &copilotManagedEntry{agentID: agentID, scriptPath: scriptPath}, nil -} - func removeManagedCopilotHooks( hooks copilotHooks, agentID string, @@ -277,7 +156,7 @@ func removeManagedCopilotHooks( return nil, err } remove := entry != nil && - (agentID == "" || sameCopilotAgentID(entry.agentID, agentID)) + (agentID == "" || sameManagedAgentID(entry.agentID, agentID)) if !remove { kept = append(kept, handler) } @@ -387,13 +266,6 @@ func renderCopilotDocument( }, nil } -func copilotEntryKey(agentID string) string { - if runtime.GOOS == "windows" { - return strings.ToLower(agentID) - } - return agentID -} - func copilotManagedEntries( handlers []map[string]any, scriptName string, @@ -405,7 +277,7 @@ func copilotManagedEntries( return nil, err } if entry != nil { - key := copilotEntryKey(entry.agentID) + key := managedReferenceKey(entry.agentID) result[key] = append(result[key], *entry) } } @@ -438,7 +310,7 @@ func copilotRuntimeContracts(hooks copilotHooks) ([]copilotRuntimeContract, erro success := successes[key] failure := failures[key] if len(guard) != 1 || len(success) != 1 || len(failure) != 1 || - !sameCopilotPath(success[0].scriptPath, failure[0].scriptPath) { + !sameManagedPath(success[0].scriptPath, failure[0].scriptPath) { continue } contracts = append(contracts, copilotRuntimeContract{ diff --git a/cmd/elydora/plugins/copilot_installation.go b/cmd/elydora/plugins/copilot_installation.go index 34f6b41..632d4a1 100644 --- a/cmd/elydora/plugins/copilot_installation.go +++ b/cmd/elydora/plugins/copilot_installation.go @@ -1,103 +1,19 @@ package plugins import ( - "encoding/json" "fmt" - "os" - "path/filepath" - "strings" ) -type copilotRuntimePaths struct { - runtimeRoot string - agentDirectory string - configPath string - keyPath string - guardPath string - auditPath string -} - type preparedCopilotInstallation struct { changes []*fileChange preconditions []filePrecondition - paths *copilotRuntimePaths -} - -func validateCopilotInstallConfig(config InstallConfig) error { - for _, field := range []struct{ name, value string }{ - {"agent name", config.AgentName}, - {"organization ID", config.OrgID}, - {"agent ID", config.AgentID}, - {"key ID", config.KID}, - {"private key", config.PrivateKey}, - {"base URL", config.BaseURL}, - {"guard script path", config.GuardScriptPath}, - } { - if field.value == "" { - return fmt.Errorf("%s is required", field.name) - } - } - if config.AgentName != copilotAgentKey { - return fmt.Errorf( - "GitHub Copilot CLI installation requires agent name %s", - copilotAgentKey, - ) - } - if strings.TrimSpace(config.OrgID) == "" { - return fmt.Errorf("organization ID is required") - } - if strings.TrimSpace(config.KID) == "" { - return fmt.Errorf("key ID is required") - } - if config.Token != "" && strings.TrimSpace(config.Token) == "" { - return fmt.Errorf("token must contain a non-whitespace value when provided") - } - if err := validateManagedPrivateKey(config.PrivateKey); err != nil { - return err - } - return validateManagedBaseURL(config.BaseURL) -} - -func copilotAgentPaths(config InstallConfig) (*copilotRuntimePaths, error) { - if err := validateCopilotInstallConfig(config); err != nil { - return nil, err - } - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return nil, err - } - agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID) - if err != nil { - return nil, err - } - paths := &copilotRuntimePaths{ - runtimeRoot: runtimeRoot, agentDirectory: agentDirectory, - configPath: filepath.Join(agentDirectory, "config.json"), - keyPath: filepath.Join(agentDirectory, "private.key"), - guardPath: filepath.Join(agentDirectory, copilotGuardScript), - auditPath: filepath.Join(agentDirectory, copilotAuditScript), - } - if !filepath.IsAbs(config.GuardScriptPath) || - !sameCopilotPath(config.GuardScriptPath, paths.guardPath) { - return nil, fmt.Errorf( - "Elydora guard runtime must use the managed agent directory: %s", - paths.guardPath, - ) - } - if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) || - !sameCopilotPath(config.HookScript, paths.auditPath)) { - return nil, fmt.Errorf( - "Elydora audit runtime must use the managed agent directory: %s", - paths.auditPath, - ) - } - return paths, nil + paths *managedRuntimePaths } func preflightCopilotInstallation( config InstallConfig, sources *copilotSources, -) (*copilotRuntimePaths, string, error) { +) (*managedRuntimePaths, string, error) { if sources == nil || sources.user == nil || sources.user.filePath == "" { return nil, "", fmt.Errorf( "GitHub Copilot CLI installation requires a user hook path", @@ -106,22 +22,22 @@ func preflightCopilotInstallation( if err := requireCopilotHooksEnabled(sources); err != nil { return nil, "", err } - paths, err := copilotAgentPaths(config) + if err := validateManagedInstallConfig(config, copilotAgentKey, "GitHub Copilot CLI"); err != nil { + return nil, "", err + } + paths, err := resolveManagedRuntimePaths(config, copilotGuardScript, copilotAuditScript) if err != nil { return nil, "", err } - if err := validateCopilotRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil { + if err := validateManagedRuntimeIdentity( + paths.agentDirectory, config.AgentID, copilotAgentKey, "Copilot", + ); err != nil { return nil, "", err } - nodePath, err := resolveNodeRuntime() + nodePath, err := resolveAbsoluteNodeRuntime("GitHub Copilot CLI") if err != nil { return nil, "", err } - if !filepath.IsAbs(nodePath) || !isCopilotNodeExecutable(nodePath) { - return nil, "", fmt.Errorf( - "GitHub Copilot CLI hooks require an absolute Node.js executable path", - ) - } hookSources := []copilotHooks{sources.user.hooks} if sources.legacy != nil { hookSources = append(hookSources, sources.legacy.hooks) @@ -132,28 +48,6 @@ func preflightCopilotInstallation( return paths, nodePath, nil } -func buildCopilotRuntimeConfig(config InstallConfig) ([]byte, error) { - value := map[string]any{ - "org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID, - "base_url": config.BaseURL, "agent_name": copilotAgentKey, - } - if config.Token != "" { - value["token"] = config.Token - } - encoded, err := json.MarshalIndent(value, "", " ") - if err != nil { - return nil, fmt.Errorf("encode Elydora runtime config: %w", err) - } - encoded = append(encoded, '\n') - if len(encoded) > maxRuntimeConfigBytes { - return nil, fmt.Errorf( - "Elydora runtime config exceeds %d bytes after JSON encoding", - maxRuntimeConfigBytes, - ) - } - return encoded, nil -} - func validateCopilotRenderedSet( sources *copilotSources, rendered []*copilotRenderedDocument, @@ -169,7 +63,7 @@ func validateCopilotRenderedSet( matches := 0 for _, item := range rendered { if item != nil && item.document != nil && - sameCopilotPath(item.document.filePath, document.filePath) { + sameManagedPath(item.document.filePath, document.filePath) { matches++ } } @@ -218,34 +112,13 @@ func prepareCopilotInstallation( if err := validateCopilotRenderedSet(sources, rendered); err != nil { return nil, err } - runtimeConfig, err := buildCopilotRuntimeConfig(config) + guardScript, auditScript := copilotExpectedScripts(config.AgentID, nil) + changes, err := managedRuntimeFileChanges( + config, paths, copilotAgentKey, string(guardScript), string(auditScript), + ) if err != nil { return nil, err } - items := []struct { - path, label string - content []byte - mode os.FileMode - }{ - { - paths.guardPath, "Elydora guard runtime", - []byte(generateGuardScript(copilotAgentKey, config.AgentID, "", false, "")), 0700, - }, - {paths.configPath, "Elydora runtime config", runtimeConfig, 0600}, - {paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600}, - { - paths.auditPath, "Elydora audit runtime", - []byte(buildHookScriptWithOutput(copilotAgentKey, config.AgentID, "", false, true)), 0700, - }, - } - changes := make([]*fileChange, 0, len(items)+len(rendered)) - for _, item := range items { - change, changeErr := prepareFileChange(item.path, item.label, item.content, item.mode) - if changeErr != nil { - return nil, changeErr - } - changes = append(changes, change) - } for _, document := range rendered { change, changeErr := prepareRenderedCopilotChange(document) if changeErr != nil { @@ -275,3 +148,8 @@ func commitCopilotInstallation( prepared.preconditions, ) } + +func copilotExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) { + return []byte(generateGuardScript(copilotAgentKey, agentID, "", false, "")), + []byte(buildHookScriptWithOutput(copilotAgentKey, agentID, "", false, true)) +} diff --git a/cmd/elydora/plugins/copilot_io.go b/cmd/elydora/plugins/copilot_io.go index fd6f8a7..1327fa2 100644 --- a/cmd/elydora/plugins/copilot_io.go +++ b/cmd/elydora/plugins/copilot_io.go @@ -240,14 +240,7 @@ func writeCopilotChanges( agentDirectory string, preconditions []filePrecondition, ) error { - hasChanges := false - for _, change := range changes { - if change != nil { - hasChanges = true - break - } - } - if !hasChanges { + if !hasFileChanges(changes) { return writeChanges(changes, label, rename, preconditions...) } if agentDirectory != "" { diff --git a/cmd/elydora/plugins/copilot_status.go b/cmd/elydora/plugins/copilot_status.go index 734173c..a423e37 100644 --- a/cmd/elydora/plugins/copilot_status.go +++ b/cmd/elydora/plugins/copilot_status.go @@ -1,261 +1,5 @@ package plugins -import ( - "bytes" - "fmt" - "path/filepath" - "sort" - "strings" -) - -func readCopilotRuntimeConfig(path string) (map[string]any, bool, error) { - snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes) - if err != nil || snapshot == nil { - return nil, snapshot != nil, err - } - label := fmt.Sprintf("Elydora runtime config at %s", path) - config, err := decodeStrictJSONObject(snapshot.contents, label) - return config, true, err -} - -func requireCopilotRuntimeString( - config map[string]any, - field string, - configPath string, -) (string, error) { - value, ok := config[field].(string) - if !ok || strings.TrimSpace(value) == "" { - return "", fmt.Errorf( - "Elydora runtime config %s is invalid: %s", - field, - configPath, - ) - } - return value, nil -} - -func validateCopilotRuntimeConfig( - config map[string]any, - expectedAgentID string, - configPath string, -) error { - supported := stringSet( - "org_id", "agent_id", "kid", "base_url", "token", "agent_name", - ) - fields := make([]string, 0, len(config)) - for field := range config { - fields = append(fields, field) - } - sort.Strings(fields) - for _, field := range fields { - if _, ok := supported[field]; !ok { - return fmt.Errorf( - `Elydora runtime config has unsupported field %q: %s`, - field, - configPath, - ) - } - } - if _, err := requireCopilotRuntimeString(config, "org_id", configPath); err != nil { - return err - } - if _, err := requireCopilotRuntimeString(config, "kid", configPath); err != nil { - return err - } - agentID, err := requireCopilotRuntimeString(config, "agent_id", configPath) - if err != nil { - return err - } - if !sameCopilotAgentID(agentID, expectedAgentID) || - config["agent_name"] != copilotAgentKey { - return fmt.Errorf( - "Elydora runtime identity does not match Copilot hooks: %s", - configPath, - ) - } - if _, exists := config["token"]; exists { - if _, err := requireCopilotRuntimeString(config, "token", configPath); err != nil { - return err - } - } - baseURL, err := requireCopilotRuntimeString(config, "base_url", configPath) - if err != nil { - return err - } - if err := validateManagedBaseURL(baseURL); err != nil { - return fmt.Errorf( - "Elydora runtime config base URL is invalid at %s: %w", - configPath, - err, - ) - } - return nil -} - -func validateCopilotRuntimeIdentity(agentDirectory, agentID string) error { - runtimeRoot := filepath.Dir(agentDirectory) - rootExists, err := managedPhysicalDirectoryExists( - runtimeRoot, - "Elydora runtime directory", - ) - if err != nil || !rootExists { - return err - } - directoryExists, err := managedPhysicalDirectoryExists( - agentDirectory, - "Elydora agent runtime directory", - ) - if err != nil || !directoryExists { - return err - } - configPath := filepath.Join(agentDirectory, "config.json") - config, configExists, err := readCopilotRuntimeConfig(configPath) - if err != nil { - return err - } - artifactExists := false - for _, item := range []struct { - path string - label string - limit int64 - }{ - {filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes}, - {filepath.Join(agentDirectory, copilotGuardScript), "Elydora guard runtime", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, copilotAuditScript), "Elydora audit runtime", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes}, - {filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes}, - {filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes}, - } { - exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit) - if inspectErr != nil { - return inspectErr - } - artifactExists = artifactExists || exists - } - if !configExists { - if artifactExists { - return fmt.Errorf( - "Elydora runtime identity cannot be verified without config.json: %s", - agentDirectory, - ) - } - return nil - } - configuredID, ok := config["agent_id"].(string) - if !ok || config["agent_name"] != copilotAgentKey || - !sameCopilotAgentID(configuredID, agentID) { - return fmt.Errorf( - "Elydora runtime config identity does not match Copilot agent %s: %s", - agentID, - configPath, - ) - } - return nil -} - -func validCopilotContractPaths(contract copilotRuntimeContract) (bool, error) { - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return false, err - } - agentDirectory := filepath.Dir(contract.guardPath) - return sameCopilotPath(filepath.Dir(agentDirectory), runtimeRoot) && - sameCopilotPath( - contract.guardPath, - filepath.Join(agentDirectory, copilotGuardScript), - ) && - sameCopilotPath( - contract.auditPath, - filepath.Join(agentDirectory, copilotAuditScript), - ), nil -} - -func copilotRuntimeContractExists(contract copilotRuntimeContract) (bool, error) { - validPaths, err := validCopilotContractPaths(contract) - if err != nil || !validPaths { - return false, err - } - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return false, err - } - agentDirectory := filepath.Dir(contract.guardPath) - rootExists, err := managedPhysicalDirectoryExists( - runtimeRoot, - "Elydora runtime directory", - ) - if err != nil || !rootExists { - return false, err - } - directoryExists, err := managedPhysicalDirectoryExists( - agentDirectory, - "Elydora agent runtime directory", - ) - if err != nil || !directoryExists { - return false, err - } - configPath := filepath.Join(agentDirectory, "config.json") - keyPath := filepath.Join(agentDirectory, "private.key") - config, configExists, err := readCopilotRuntimeConfig(configPath) - if err != nil { - return false, err - } - key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes) - if err != nil { - return false, err - } - guard, err := readManagedFile( - contract.guardPath, - "Elydora guard runtime", - maxManagedSourceBytes, - ) - if err != nil { - return false, err - } - audit, err := readManagedFile( - contract.auditPath, - "Elydora audit runtime", - maxManagedSourceBytes, - ) - if err != nil { - return false, err - } - if !configExists || key == nil || guard == nil || audit == nil { - return false, nil - } - if err := validateCopilotRuntimeConfig(config, contract.agentID, configPath); err != nil { - return false, err - } - if err := validateManagedPrivateKey(string(key.contents)); err != nil { - return false, fmt.Errorf("Elydora private key at %s: %w", keyPath, err) - } - expectedGuard := []byte(generateGuardScript( - copilotAgentKey, - contract.agentID, - "", - false, - "", - )) - expectedAudit := []byte(buildHookScriptWithOutput( - copilotAgentKey, - contract.agentID, - "", - false, - true, - )) - return bytes.Equal(guard.contents, expectedGuard) && - bytes.Equal(audit.contents, expectedAudit), nil -} - -func copilotRuntimeFilesExist(contracts []copilotRuntimeContract) (bool, error) { - for _, contract := range contracts { - exists, err := copilotRuntimeContractExists(contract) - if err != nil { - return false, err - } - if exists { - return true, nil - } - } - return false, nil +func copilotRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) { + return managedRuntimeFilesExist(contracts, copilotAgentKey, "Copilot", copilotExpectedScripts) } diff --git a/cmd/elydora/plugins/cursor_test.go b/cmd/elydora/plugins/cursor_test.go index 464b42a..18840fd 100644 --- a/cmd/elydora/plugins/cursor_test.go +++ b/cmd/elydora/plugins/cursor_test.go @@ -342,7 +342,7 @@ func TestCursorCreatesMissingGuardAndRejectsUnmanagedRuntimePaths(t *testing.T) fixture.config.HookScript = unmanaged } if err := fixture.plugin.Install(fixture.config); err == nil || - !strings.Contains(err.Error(), "managed Elydora agent directory") { + !strings.Contains(err.Error(), "managed agent directory") { t.Fatalf("unmanaged %s error = %v", field, err) } if _, err := os.Stat(fixture.configPath); !os.IsNotExist(err) { diff --git a/cmd/elydora/plugins/droid.go b/cmd/elydora/plugins/droid.go index 97fcee9..962c451 100644 --- a/cmd/elydora/plugins/droid.go +++ b/cmd/elydora/plugins/droid.go @@ -7,14 +7,12 @@ type DroidPlugin struct { rename renameFunc } -// ManagesGuardRuntime reports that Droid commits its guard, audit runtime, -// credentials, and hook documents in one transaction. +// ManagesGuardRuntime reports that the guard is committed with the audit runtime. func (p *DroidPlugin) ManagesGuardRuntime() bool { return true } -// PreflightInstall validates every hook source, policy layer, credential, -// matcher, and runtime identity before any write. +// PreflightInstall validates every source before the CLI creates runtime state. func (p *DroidPlugin) PreflightInstall(config InstallConfig) error { sources, err := readDroidSources() if err != nil { @@ -107,7 +105,7 @@ func (p *DroidPlugin) Uninstall(agentID string) error { if err != nil { return err } - runtimeRoot, err := droidRuntimeRoot() + runtimeRoot, err := AgentRuntimeRoot() if err != nil { return err } @@ -133,7 +131,7 @@ func (p *DroidPlugin) Status() (PluginStatus, error) { if droidHookBlocked(sources) != nil { return status, nil } - runtimeRoot, err := droidRuntimeRoot() + runtimeRoot, err := AgentRuntimeRoot() if err != nil { return status, err } diff --git a/cmd/elydora/plugins/droid_config.go b/cmd/elydora/plugins/droid_config.go index 1b5f13f..17a79a3 100644 --- a/cmd/elydora/plugins/droid_config.go +++ b/cmd/elydora/plugins/droid_config.go @@ -239,7 +239,7 @@ func droidAdditionsFor( document, target *droidDocument, groups map[string]map[string]any, ) map[string]map[string]any { - if sameDroidPath(document.filePath, target.filePath) { + if sameManagedPath(document.filePath, target.filePath) { return groups } return map[string]map[string]any{} diff --git a/cmd/elydora/plugins/droid_contract.go b/cmd/elydora/plugins/droid_contract.go index c493065..b4f83f5 100644 --- a/cmd/elydora/plugins/droid_contract.go +++ b/cmd/elydora/plugins/droid_contract.go @@ -15,13 +15,11 @@ import ( ) const ( - droidAgentKey = "droid" - droidGuardScript = "guard.js" - droidAuditScript = "hook.js" - droidHookTimeout = float64(10) - droidOwnedFileMarker = "// Managed by Elydora" - droidPOSIXApostrophe = `'"'"'` - droidWindowsExitSuffix = "; exit $LASTEXITCODE" + droidAgentKey = "droid" + droidGuardScript = "guard.js" + droidAuditScript = "hook.js" + droidHookTimeout = float64(10) + droidOwnedFileMarker = "// Managed by Elydora" ) var droidToolEvents = [...]string{"PreToolUse", "PostToolUse"} @@ -35,11 +33,7 @@ type droidManagedRemoval struct { removeGroup bool } -type droidRuntimeContract struct { - agentID string - guardPath string - auditPath string -} +type droidRuntimeContract = managedRuntimeContract type droidRegexEntry struct { Label string `json:"label"` @@ -180,14 +174,9 @@ for (const entry of entries) { func buildDroidCommand(nodePath, scriptPath string) string { if runtime.GOOS == "windows" { - return "& " + quoteDroidPowerShellArgument(nodePath) + " " + - quoteDroidPowerShellArgument(scriptPath) + droidWindowsExitSuffix + return powerShellSource(nodePath, scriptPath) } - return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath) -} - -func quoteDroidPowerShellArgument(value string) string { - return "'" + strings.ReplaceAll(value, "'", "''") + "'" + return posixSource(nodePath, scriptPath) } func buildDroidGroup(nodePath, scriptPath string) map[string]any { @@ -201,36 +190,15 @@ func buildDroidGroup(nodePath, scriptPath string) map[string]any { func parseDroidCommand(command string, includeLegacy ...bool) (string, string, bool) { if runtime.GOOS != "windows" { - return parseDroidPOSIXCommand(command) + return parsePOSIXCommand(command) } - executable, script, ok := parseDroidPowerShellCommand(command) + executable, script, ok := parsePowerShellSource(command) if ok || len(includeLegacy) == 0 || !includeLegacy[0] { return executable, script, ok } return parseDroidLegacyWindowsCommand(command) } -func parseDroidPOSIXCommand(command string) (string, string, bool) { - executable, next, ok := readDroidPOSIXArgument(command, 0) - if !ok || next >= len(command) || command[next] != ' ' { - return "", "", false - } - script, end, ok := readDroidPOSIXArgument(command, next+1) - return executable, script, ok && end == len(command) && executable != "" && script != "" -} - -func parseDroidPowerShellCommand(command string) (string, string, bool) { - if !strings.HasPrefix(command, "& ") { - return "", "", false - } - executable, next, ok := readDroidPowerShellArgument(command, 2) - if !ok || next >= len(command) || command[next] != ' ' { - return "", "", false - } - script, end, ok := readDroidPowerShellArgument(command, next+1) - return executable, script, ok && command[end:] == droidWindowsExitSuffix && executable != "" && script != "" -} - func parseDroidLegacyWindowsCommand(command string) (string, string, bool) { executable, next, ok := readDroidLegacyWindowsArgument(command, 0) if !ok || next >= len(command) || command[next] != ' ' { @@ -240,26 +208,6 @@ func parseDroidLegacyWindowsCommand(command string) (string, string, bool) { return executable, script, ok && end == len(command) && executable != "" && script != "" } -func readDroidPowerShellArgument(command string, start int) (string, int, bool) { - if start >= len(command) || command[start] != '\'' { - return "", start, false - } - var value strings.Builder - for index := start + 1; index < len(command); index++ { - if command[index] != '\'' { - value.WriteByte(command[index]) - continue - } - if index+1 < len(command) && command[index+1] == '\'' { - value.WriteByte('\'') - index++ - continue - } - return value.String(), index + 1, true - } - return "", start, false -} - func readDroidLegacyWindowsArgument(command string, start int) (string, int, bool) { if start >= len(command) || command[start] != '"' { return "", start, false @@ -273,52 +221,6 @@ func readDroidLegacyWindowsArgument(command string, start int) (string, int, boo return value, end + 1, value != "" && !strings.ContainsAny(value, "\r\n") } -func readDroidPOSIXArgument(command string, start int) (string, int, bool) { - if start >= len(command) || command[start] != '\'' { - return "", start, false - } - var value strings.Builder - for index := start + 1; index < len(command); { - if strings.HasPrefix(command[index:], droidPOSIXApostrophe) { - value.WriteByte('\'') - index += len(droidPOSIXApostrophe) - continue - } - if command[index] == '\'' { - return value.String(), index + 1, true - } - value.WriteByte(command[index]) - index++ - } - return "", start, false -} - -func sameDroidPath(left, right string) bool { - left = filepath.Clean(left) - right = filepath.Clean(right) - if absolute, err := filepath.Abs(left); err == nil { - left = absolute - } - if absolute, err := filepath.Abs(right); err == nil { - right = absolute - } - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func sameDroidAgentID(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func droidRuntimeRoot() (string, error) { - return AgentRuntimeRoot() -} - func managedDroidAgentID( handler map[string]any, scriptName, runtimeRoot string, @@ -333,29 +235,17 @@ func managedDroidAgentID( } executable, scriptPath, ok := parseDroidCommand(command, includeLegacy...) if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) || - !isDroidNodeExecutable(executable) || !sameDroidFileName(filepath.Base(scriptPath), scriptName) { + !isNodeExecutable(executable) || !sameManagedName(filepath.Base(scriptPath), scriptName) { return "", false } agentDirectory := filepath.Dir(scriptPath) - if !sameDroidPath(filepath.Dir(agentDirectory), runtimeRoot) { + if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) { return "", false } agentID := filepath.Base(agentDirectory) return agentID, agentID != "" && agentID != "." && agentID != ".." } -func isDroidNodeExecutable(path string) bool { - name := filepath.Base(path) - return name == "node" || strings.EqualFold(name, "node.exe") -} - -func sameDroidFileName(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - func managedDroidRemovals(settings droidHookSettings, agentID, runtimeRoot string) []droidManagedRemoval { removals := make([]droidManagedRemoval, 0) for _, contract := range []struct{ event, script string }{ @@ -370,7 +260,7 @@ func managedDroidRemovals(settings droidHookSettings, agentID, runtimeRoot strin managedID, managed := managedDroidAgentID( handlerValue.(map[string]any), contract.script, runtimeRoot, true, ) - if managed && (agentID == "" || sameDroidAgentID(managedID, agentID)) { + if managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) { indexes = append(indexes, handlerIndex) } } @@ -400,7 +290,7 @@ func droidRuntimeContracts(settings droidHookSettings, runtimeRoot string) []dro contracts := make([]droidRuntimeContract, 0, len(guardIDs)) for _, guardID := range guardIDs { for auditID := range audits { - if !sameDroidAgentID(guardID, auditID) { + if !sameManagedAgentID(guardID, auditID) { continue } root := filepath.Join(runtimeRoot, guardID) diff --git a/cmd/elydora/plugins/droid_installation.go b/cmd/elydora/plugins/droid_installation.go index 4730cbf..1d27833 100644 --- a/cmd/elydora/plugins/droid_installation.go +++ b/cmd/elydora/plugins/droid_installation.go @@ -1,24 +1,13 @@ package plugins import ( - "encoding/json" "fmt" - "os" - "path/filepath" - "strings" ) -type droidRuntimePaths struct { - runtimeRoot string - agentDirectory string - guardPath string - auditPath string -} - type preparedDroidInstallation struct { changes []*fileChange preconditions []filePrecondition - paths *droidRuntimePaths + paths *managedRuntimePaths } type preparedDroidUninstall struct { @@ -39,80 +28,10 @@ func droidSourceLabel(document *droidDocument) string { } } -func validateDroidInstallConfig(config InstallConfig) error { - for _, field := range []struct{ name, value string }{ - {"agent name", config.AgentName}, - {"organization ID", config.OrgID}, - {"agent ID", config.AgentID}, - {"key ID", config.KID}, - {"private key", config.PrivateKey}, - {"base URL", config.BaseURL}, - {"guard script path", config.GuardScriptPath}, - } { - if field.value == "" { - return fmt.Errorf("%s is required", field.name) - } - } - if config.AgentName != droidAgentKey { - return fmt.Errorf( - "Factory Droid installation requires agent name %s", - droidAgentKey, - ) - } - if strings.TrimSpace(config.OrgID) == "" { - return fmt.Errorf("organization ID is required") - } - if strings.TrimSpace(config.KID) == "" { - return fmt.Errorf("key ID is required") - } - if config.Token != "" && strings.TrimSpace(config.Token) == "" { - return fmt.Errorf("token must contain a non-whitespace value when provided") - } - if err := validateManagedPrivateKey(config.PrivateKey); err != nil { - return err - } - return validateManagedBaseURL(config.BaseURL) -} - -func droidAgentPaths(config InstallConfig) (*droidRuntimePaths, error) { - if err := validateDroidInstallConfig(config); err != nil { - return nil, err - } - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return nil, err - } - agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID) - if err != nil { - return nil, err - } - paths := &droidRuntimePaths{ - runtimeRoot: runtimeRoot, - agentDirectory: agentDirectory, - guardPath: filepath.Join(agentDirectory, droidGuardScript), - auditPath: filepath.Join(agentDirectory, droidAuditScript), - } - if !filepath.IsAbs(config.GuardScriptPath) || - !sameDroidPath(config.GuardScriptPath, paths.guardPath) { - return nil, fmt.Errorf( - "Elydora guard runtime must use the managed agent directory: %s", - paths.guardPath, - ) - } - if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) || - !sameDroidPath(config.HookScript, paths.auditPath)) { - return nil, fmt.Errorf( - "Elydora audit runtime must use the managed agent directory: %s", - paths.auditPath, - ) - } - return paths, nil -} - func preflightDroidInstallation( config InstallConfig, sources *droidSources, -) (*droidRuntimePaths, string, error) { +) (*managedRuntimePaths, string, error) { if sources == nil || sources.root == nil || sources.policy == nil { return nil, "", fmt.Errorf("Factory Droid installation sources are required") } @@ -126,50 +45,25 @@ func preflightDroidInstallation( if err := validateDroidRegexes(hooks...); err != nil { return nil, "", err } - paths, err := droidAgentPaths(config) + if err := validateManagedInstallConfig(config, droidAgentKey, "Factory Droid"); err != nil { + return nil, "", err + } + paths, err := resolveManagedRuntimePaths(config, droidGuardScript, droidAuditScript) if err != nil { return nil, "", err } - if err := validateDroidRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil { + if err := validateManagedRuntimeIdentity( + paths.agentDirectory, config.AgentID, droidAgentKey, "Factory Droid", + ); err != nil { return nil, "", err } - nodePath, err := resolveNodeRuntime() + nodePath, err := resolveAbsoluteNodeRuntime("Factory Droid") if err != nil { return nil, "", err } - if !filepath.IsAbs(nodePath) || !isDroidNodeExecutable(nodePath) { - return nil, "", fmt.Errorf( - "Factory Droid hooks require an absolute Node.js executable path", - ) - } return paths, nodePath, nil } -func buildDroidRuntimeConfig(config InstallConfig) ([]byte, error) { - value := map[string]any{ - "org_id": config.OrgID, - "agent_id": config.AgentID, - "kid": config.KID, - "base_url": config.BaseURL, - "agent_name": droidAgentKey, - } - if config.Token != "" { - value["token"] = config.Token - } - encoded, err := json.MarshalIndent(value, "", " ") - if err != nil { - return nil, fmt.Errorf("encode Elydora runtime config: %w", err) - } - encoded = append(encoded, '\n') - if len(encoded) > maxRuntimeConfigBytes { - return nil, fmt.Errorf( - "Elydora runtime config exceeds %d bytes after JSON encoding", - maxRuntimeConfigBytes, - ) - } - return encoded, nil -} - func prepareRenderedDroidChange( rendered *droidRenderedDocument, ) (*fileChange, error) { @@ -198,7 +92,7 @@ func validateDroidRenderedSet( matches := 0 for _, item := range rendered { if item != nil && item.document != nil && - sameDroidPath(item.document.filePath, document.filePath) { + sameManagedPath(item.document.filePath, document.filePath) { matches++ } } @@ -217,7 +111,7 @@ func droidSourcePreconditions( for _, document := range droidSourceDocuments(sources) { changed := false for _, path := range changedPaths { - if sameDroidPath(document.filePath, path) { + if sameManagedPath(document.filePath, path) { changed = true break } @@ -246,38 +140,13 @@ func prepareDroidInstallation( if err := validateDroidRenderedSet(sources, rendered); err != nil { return nil, err } - runtimeConfig, err := buildDroidRuntimeConfig(config) + guardScript, auditScript := droidExpectedScripts(config.AgentID, nil) + changes, err := managedRuntimeFileChanges( + config, paths, droidAgentKey, string(guardScript), string(auditScript), + ) if err != nil { return nil, err } - items := []struct { - path, label string - content []byte - mode os.FileMode - }{ - { - paths.guardPath, - "Elydora guard runtime", - []byte(generateGuardScript(droidAgentKey, config.AgentID, "", false, "")), - 0700, - }, - {filepath.Join(paths.agentDirectory, "config.json"), "Elydora runtime config", runtimeConfig, 0600}, - {filepath.Join(paths.agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey), 0600}, - { - paths.auditPath, - "Elydora audit runtime", - []byte(buildHookScriptWithOutput(droidAgentKey, config.AgentID, "", false, true)), - 0700, - }, - } - changes := make([]*fileChange, 0, len(items)+len(rendered)) - for _, item := range items { - change, changeErr := prepareFileChange(item.path, item.label, item.content, item.mode) - if changeErr != nil { - return nil, changeErr - } - changes = append(changes, change) - } changedPaths := make([]string, 0, len(rendered)) for _, document := range rendered { change, changeErr := prepareRenderedDroidChange(document) @@ -305,14 +174,7 @@ func writeDroidChanges( runtimeRoot, agentDirectory string, preconditions []filePrecondition, ) error { - hasChanges := false - for _, change := range changes { - if change != nil { - hasChanges = true - break - } - } - if !hasChanges { + if !hasFileChanges(changes) { return writeChanges(changes, label, rename, preconditions...) } if err := assertFilePreconditions(preconditions, label); err != nil { @@ -365,7 +227,7 @@ func prepareDroidUninstall( for _, item := range rendered { changed := false for _, path := range changedPaths { - if sameDroidPath(item.document.filePath, path) { + if sameManagedPath(item.document.filePath, path) { changed = true break } @@ -398,3 +260,8 @@ func commitDroidUninstall( prepared.preconditions, ) } + +func droidExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) { + return []byte(generateGuardScript(droidAgentKey, agentID, "", false, "")), + []byte(buildHookScriptWithOutput(droidAgentKey, agentID, "", false, true)) +} diff --git a/cmd/elydora/plugins/droid_status.go b/cmd/elydora/plugins/droid_status.go index 2f9ed24..71259d5 100644 --- a/cmd/elydora/plugins/droid_status.go +++ b/cmd/elydora/plugins/droid_status.go @@ -1,251 +1,5 @@ package plugins -import ( - "bytes" - "fmt" - "path/filepath" - "sort" - "strings" -) - -func readDroidRuntimeConfig(path string) (map[string]any, bool, error) { - snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes) - if err != nil || snapshot == nil { - return nil, snapshot != nil, err - } - config, err := decodeStrictJSONObject( - snapshot.contents, - fmt.Sprintf("Elydora runtime config at %s", path), - ) - return config, true, err -} - -func requireDroidRuntimeString( - config map[string]any, - field, configPath string, -) (string, error) { - value, ok := config[field].(string) - if !ok || strings.TrimSpace(value) == "" { - return "", fmt.Errorf( - "Elydora runtime config %s is invalid: %s", - field, - configPath, - ) - } - return value, nil -} - -func validateDroidRuntimeConfig( - config map[string]any, - expectedAgentID, configPath string, -) error { - supported := stringSet( - "org_id", - "agent_id", - "kid", - "base_url", - "token", - "agent_name", - ) - fields := make([]string, 0, len(config)) - for field := range config { - fields = append(fields, field) - } - sort.Strings(fields) - for _, field := range fields { - if _, ok := supported[field]; !ok { - return fmt.Errorf( - `Elydora runtime config has unsupported field %q: %s`, - field, - configPath, - ) - } - } - if _, err := requireDroidRuntimeString(config, "org_id", configPath); err != nil { - return err - } - if _, err := requireDroidRuntimeString(config, "kid", configPath); err != nil { - return err - } - agentID, err := requireDroidRuntimeString(config, "agent_id", configPath) - if err != nil { - return err - } - if !sameDroidAgentID(agentID, expectedAgentID) || config["agent_name"] != droidAgentKey { - return fmt.Errorf( - "Elydora runtime identity does not match Factory Droid hooks: %s", - configPath, - ) - } - if _, exists := config["token"]; exists { - if _, err := requireDroidRuntimeString(config, "token", configPath); err != nil { - return err - } - } - baseURL, err := requireDroidRuntimeString(config, "base_url", configPath) - if err != nil { - return err - } - if err := validateManagedBaseURL(baseURL); err != nil { - return fmt.Errorf( - "Elydora runtime config base URL is invalid at %s: %w", - configPath, - err, - ) - } - return nil -} - -func validateDroidRuntimeIdentity(agentDirectory, agentID string) error { - runtimeRoot := filepath.Dir(agentDirectory) - rootExists, err := managedPhysicalDirectoryExists( - runtimeRoot, - "Elydora runtime directory", - ) - if err != nil || !rootExists { - return err - } - directoryExists, err := managedPhysicalDirectoryExists( - agentDirectory, - "Elydora agent runtime directory", - ) - if err != nil || !directoryExists { - return err - } - configPath := filepath.Join(agentDirectory, "config.json") - config, configExists, err := readDroidRuntimeConfig(configPath) - if err != nil { - return err - } - artifactExists := false - for _, item := range []struct { - path string - label string - limit int64 - }{ - {filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes}, - {filepath.Join(agentDirectory, droidGuardScript), "Elydora guard runtime", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, droidAuditScript), "Elydora audit runtime", maxManagedSourceBytes}, - {filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes}, - {filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes}, - {filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes}, - } { - exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit) - if inspectErr != nil { - return inspectErr - } - artifactExists = artifactExists || exists - } - if !configExists { - if artifactExists { - return fmt.Errorf( - "Elydora runtime identity cannot be verified without config.json: %s", - agentDirectory, - ) - } - return nil - } - configuredID, ok := config["agent_id"].(string) - if !ok || config["agent_name"] != droidAgentKey || - !sameDroidAgentID(configuredID, agentID) { - return fmt.Errorf( - "Elydora runtime config identity does not match Factory Droid agent %s: %s", - agentID, - configPath, - ) - } - return nil -} - -func validDroidContractPaths(contract droidRuntimeContract) (bool, error) { - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return false, err - } - agentDirectory := filepath.Dir(contract.guardPath) - return sameDroidPath(filepath.Dir(agentDirectory), runtimeRoot) && - sameDroidPath( - contract.guardPath, - filepath.Join(agentDirectory, droidGuardScript), - ) && - sameDroidPath( - contract.auditPath, - filepath.Join(agentDirectory, droidAuditScript), - ), nil -} - -func droidRuntimeContractExists(contract droidRuntimeContract) (bool, error) { - validPaths, err := validDroidContractPaths(contract) - if err != nil || !validPaths { - return false, err - } - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return false, err - } - agentDirectory := filepath.Dir(contract.guardPath) - rootExists, err := managedPhysicalDirectoryExists( - runtimeRoot, - "Elydora runtime directory", - ) - if err != nil || !rootExists { - return false, err - } - directoryExists, err := managedPhysicalDirectoryExists( - agentDirectory, - "Elydora agent runtime directory", - ) - if err != nil || !directoryExists { - return false, err - } - configPath := filepath.Join(agentDirectory, "config.json") - keyPath := filepath.Join(agentDirectory, "private.key") - config, configExists, err := readDroidRuntimeConfig(configPath) - if err != nil { - return false, err - } - key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes) - if err != nil { - return false, err - } - guard, err := readManagedFile(contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes) - if err != nil { - return false, err - } - audit, err := readManagedFile(contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes) - if err != nil { - return false, err - } - if !configExists || key == nil || guard == nil || audit == nil { - return false, nil - } - if err := validateDroidRuntimeConfig(config, contract.agentID, configPath); err != nil { - return false, err - } - if err := validateManagedPrivateKey(string(key.contents)); err != nil { - return false, fmt.Errorf("Elydora private key at %s: %w", keyPath, err) - } - expectedGuard := []byte(generateGuardScript(droidAgentKey, contract.agentID, "", false, "")) - expectedAudit := []byte(buildHookScriptWithOutput( - droidAgentKey, - contract.agentID, - "", - false, - true, - )) - return bytes.Equal(guard.contents, expectedGuard) && - bytes.Equal(audit.contents, expectedAudit), nil -} - -func droidRuntimeFilesExist(contracts []droidRuntimeContract) (bool, error) { - for _, contract := range contracts { - exists, err := droidRuntimeContractExists(contract) - if err != nil { - return false, err - } - if exists { - return true, nil - } - } - return false, nil +func droidRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) { + return managedRuntimeFilesExist(contracts, droidAgentKey, "Factory Droid", droidExpectedScripts) } diff --git a/cmd/elydora/plugins/droid_test.go b/cmd/elydora/plugins/droid_test.go index 0c7df37..8d27dfa 100644 --- a/cmd/elydora/plugins/droid_test.go +++ b/cmd/elydora/plugins/droid_test.go @@ -57,7 +57,7 @@ func TestDroidInstallWritesCurrentContainerAndCompleteRuntime(t *testing.T) { requireDroidNativeGroup(t, auditGroup) guardCommand := droidManagedHandler(t, hooks["PreToolUse"], fixture.guardPath)["command"].(string) if runtime.GOOS == "windows" && - (!strings.HasPrefix(guardCommand, "& '") || !strings.HasSuffix(guardCommand, droidWindowsExitSuffix)) { + (!strings.HasPrefix(guardCommand, "& '") || !strings.HasSuffix(guardCommand, powerShellExitSuffix)) { t.Fatalf("Windows Factory command = %q", guardCommand) } runtimeConfig := readDroidTestObject(t, fixture.runtimeConfig) diff --git a/cmd/elydora/plugins/droid_test_helpers_test.go b/cmd/elydora/plugins/droid_test_helpers_test.go index 6c41418..2fcdb43 100644 --- a/cmd/elydora/plugins/droid_test_helpers_test.go +++ b/cmd/elydora/plugins/droid_test_helpers_test.go @@ -237,7 +237,7 @@ func droidManagedGroup(t *testing.T, groupsValue any, scriptPath string) map[str handler := requireDroidObject(t, handlerValue) command, _ := handler["command"].(string) _, configuredPath, managed := parseDroidCommand(command, true) - if managed && sameDroidPath(configuredPath, scriptPath) { + if managed && sameManagedPath(configuredPath, scriptPath) { return group } } @@ -253,7 +253,7 @@ func droidManagedHandler(t *testing.T, groupsValue any, scriptPath string) map[s handler := requireDroidObject(t, handlerValue) command, _ := handler["command"].(string) _, configuredPath, managed := parseDroidCommand(command, true) - if managed && sameDroidPath(configuredPath, scriptPath) { + if managed && sameManagedPath(configuredPath, scriptPath) { return handler } } diff --git a/cmd/elydora/plugins/droid_transaction_test.go b/cmd/elydora/plugins/droid_transaction_test.go index 136ed4d..3074da8 100644 --- a/cmd/elydora/plugins/droid_transaction_test.go +++ b/cmd/elydora/plugins/droid_transaction_test.go @@ -53,7 +53,7 @@ func TestDroidInstallRollsBackRuntimeAfterLateHookCommitFailure(t *testing.T) { prepared := prepareDroidTestInstallation(t, fixture, fixture.config) failed := false rename := func(source, destination string) error { - if !failed && sameDroidPath(destination, fixture.configPath) { + if !failed && sameManagedPath(destination, fixture.configPath) { failed = true return errors.New("injected Droid hook commit failure") } @@ -85,7 +85,7 @@ func TestDroidConcurrentActiveHookReplacementPreventsRuntimeCommit(t *testing.T) t.Fatalf("concurrent source error = %v", err) } for path, source := range before { - if sameDroidPath(path, fixture.configPath) { + if sameManagedPath(path, fixture.configPath) { continue } if readDroidTestFile(t, path) != source { @@ -205,7 +205,7 @@ func TestDroidSameContentReplacementIsRejectedByPhysicalIdentity(t *testing.T) { t.Fatal("same-content replacement changed") } for path, source := range before { - if !sameDroidPath(path, fixture.configPath) && readDroidTestFile(t, path) != source { + if !sameManagedPath(path, fixture.configPath) && readDroidTestFile(t, path) != source { t.Fatalf("runtime changed after inode replacement: %s", path) } } @@ -226,7 +226,7 @@ func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) { if err != nil { t.Fatalf("read uninstall sources: %v", err) } - runtimeRoot, err := droidRuntimeRoot() + runtimeRoot, err := AgentRuntimeRoot() if err != nil { t.Fatalf("resolve runtime root: %v", err) } @@ -240,7 +240,7 @@ func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) { } failed := false rename := func(source, destination string) error { - if !failed && sameDroidPath(destination, fixture.settingsPath) { + if !failed && sameManagedPath(destination, fixture.settingsPath) { failed = true return errors.New("injected Droid uninstall failure") } diff --git a/cmd/elydora/plugins/gemini_security_test.go b/cmd/elydora/plugins/gemini_security_test.go index d740c79..0c5736a 100644 --- a/cmd/elydora/plugins/gemini_security_test.go +++ b/cmd/elydora/plugins/gemini_security_test.go @@ -164,10 +164,10 @@ func TestGeminiValidatesManagedRuntimeInputsBeforeWrites(t *testing.T) { }, "query parameters"}, {"guard path", func(f *geminiFixture) { f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js") - }, "managed Elydora agent directory"}, + }, "managed agent directory"}, {"audit path", func(f *geminiFixture) { f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js") - }, "managed Elydora agent directory"}, + }, "managed agent directory"}, } { t.Run(testCase.name, func(t *testing.T) { fixture := prepareGeminiFixture(t, geminiFixtureOptions{}) diff --git a/cmd/elydora/plugins/managed_runtime.go b/cmd/elydora/plugins/managed_runtime.go index e242070..685af02 100644 --- a/cmd/elydora/plugins/managed_runtime.go +++ b/cmd/elydora/plugins/managed_runtime.go @@ -136,14 +136,14 @@ func resolveManagedRuntimePaths( if !filepath.IsAbs(config.GuardScriptPath) || !sameManagedPath(config.GuardScriptPath, paths.guardPath) { return nil, fmt.Errorf( - "guard runtime must use the managed Elydora agent directory: %s", + "Elydora guard runtime must use the managed agent directory: %s", paths.guardPath, ) } if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) || !sameManagedPath(config.HookScript, paths.auditPath)) { return nil, fmt.Errorf( - "audit runtime must use the managed Elydora agent directory: %s", + "Elydora audit runtime must use the managed agent directory: %s", paths.auditPath, ) }