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
This commit is contained in:
雷电芽衣 committed 2026-09-03 01:56:07 -04:00
1 parent a7fc75bf6b
commit 1ede03d989
28 files changed
+210 -2027

No files matched your search

+5 -4
View File
@@ -10,8 +10,7 @@ type AugmentPlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Auggie commits its provider guard together // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// with both wrappers, the audit runtime, and user settings.
func (p *AugmentPlugin) ManagesGuardRuntime() bool { func (p *AugmentPlugin) ManagesGuardRuntime() bool {
return true return true
} }
@@ -57,13 +56,14 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
if err != nil { if err != nil {
return err return err
} }
if err := writeAugmentChanges( if err := writeManagedChanges(
changes, changes,
"Install Augment Code CLI hooks", "Install Augment Code CLI hooks",
p.rename, p.rename,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.configPath), filepath.Dir(document.configPath),
"Auggie configuration directory",
); err != nil { ); err != nil {
return err return err
} }
@@ -99,13 +99,14 @@ func (p *AugmentPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
return writeAugmentChanges( return writeManagedChanges(
[]*fileChange{change}, []*fileChange{change},
"Uninstall Augment Code CLI hooks", "Uninstall Augment Code CLI hooks",
p.rename, p.rename,
"", "",
"", "",
filepath.Dir(document.configPath), filepath.Dir(document.configPath),
"Auggie configuration directory",
) )
} }
+5 -51
View File
@@ -16,7 +16,6 @@ const (
augmentGuardScript = "guard.js" augmentGuardScript = "guard.js"
augmentAuditScript = "hook.js" augmentAuditScript = "hook.js"
augmentHookTimeout = float64(10_000) augmentHookTimeout = float64(10_000)
augmentPOSIXApostrophe = `'"'"'`
) )
var ( var (
@@ -314,23 +313,8 @@ func readAugmentWindowsArgument(command string) (string, bool) {
} }
func readAugmentPOSIXArgument(command string) (string, bool) { func readAugmentPOSIXArgument(command string) (string, bool) {
if len(command) < 2 || command[0] != '\'' { value, end, ok := readPOSIXArgument(command, 0)
return "", false return value, ok && end == len(command) && value != ""
}
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
} }
func parseAugmentCommand(command string) (string, bool) { func parseAugmentCommand(command string) (string, bool) {
@@ -340,36 +324,6 @@ func parseAugmentCommand(command string) (string, bool) {
return readAugmentPOSIXArgument(command) 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( func managedAugmentAgentID(
handler map[string]any, handler map[string]any,
wrapperName, runtimeRoot string, wrapperName, runtimeRoot string,
@@ -385,11 +339,11 @@ func managedAugmentAgentID(
return "", false return "", false
} }
wrapperPath, ok := parseAugmentCommand(command) wrapperPath, ok := parseAugmentCommand(command)
if !ok || !sameAugmentFileName(filepath.Base(wrapperPath), wrapperName) { if !ok || !sameManagedName(filepath.Base(wrapperPath), wrapperName) {
return "", false return "", false
} }
agentDirectory := filepath.Dir(wrapperPath) agentDirectory := filepath.Dir(wrapperPath)
if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) { if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false return "", false
} }
agentID := filepath.Base(agentDirectory) agentID := filepath.Base(agentDirectory)
@@ -407,7 +361,7 @@ func removeManagedAugmentGroups(
groupChanged := false groupChanged := false
for _, handler := range group.handlers { for _, handler := range group.handlers {
managedID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot) managedID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
remove := managed && (agentID == "" || sameAugmentAgentID(managedID, agentID)) remove := managed && (agentID == "" || sameManagedAgentID(managedID, agentID))
if remove { if remove {
changed = true changed = true
groupChanged = true groupChanged = true
+35 -155
View File
@@ -1,96 +1,42 @@
package plugins package plugins
import ( import "fmt"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type augmentRuntimePaths struct { type augmentRuntimePaths struct {
runtimeRoot string managedRuntimePaths
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
guardWrapperPath string guardWrapperPath string
auditWrapperPath string auditWrapperPath string
} }
func validateAugmentInstallConfig(config InstallConfig) error { func augmentWrapperArtifacts(agentDirectory string) []managedArtifact {
for _, field := range []struct{ name, value string }{ wrappers := resolveAugmentWrapperPaths(agentDirectory)
{"agent name", config.AgentName}, return []managedArtifact{
{"organization ID", config.OrgID}, {wrappers.guard, "Auggie guard wrapper", maxManagedSourceBytes},
{"agent ID", config.AgentID}, {wrappers.audit, "Auggie audit wrapper", maxManagedSourceBytes},
{"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 != 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) { 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 return nil, err
} }
runtimeRoot, err := AgentRuntimeRoot() paths, err := resolveManagedRuntimePaths(config, augmentGuardScript, augmentAuditScript)
if err != nil { if err != nil {
return nil, err return nil, err
} }
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID) wrappers := resolveAugmentWrapperPaths(paths.agentDirectory)
if err != nil { return &augmentRuntimePaths{
return nil, err managedRuntimePaths: *paths,
}
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, guardWrapperPath: wrappers.guard,
auditWrapperPath: wrappers.audit, auditWrapperPath: wrappers.audit,
} }, nil
if !filepath.IsAbs(config.GuardScriptPath) || }
!sameAugmentPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf( func validateAugmentRuntimeIdentity(agentDirectory, agentID string) error {
"Elydora guard runtime must use the managed agent directory: %s", return validateManagedRuntimeIdentity(
paths.guardPath, agentDirectory, agentID, augmentAgentKey, "Auggie",
augmentWrapperArtifacts(agentDirectory)...,
) )
}
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
} }
func preflightAugmentInstallation( func preflightAugmentInstallation(
@@ -98,55 +44,25 @@ func preflightAugmentInstallation(
document *augmentDocument, document *augmentDocument,
) (*augmentRuntimePaths, string, error) { ) (*augmentRuntimePaths, string, error) {
if document == nil || document.configPath == "" { if document == nil || document.configPath == "" {
return nil, "", fmt.Errorf( return nil, "", fmt.Errorf("Augment Code CLI installation requires a user settings document")
"Augment Code CLI installation requires a user settings document",
)
} }
paths, err := augmentAgentPaths(config) paths, err := augmentAgentPaths(config)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if err := validateAugmentRuntimeIdentity( if err := validateAugmentRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
paths.agentDirectory,
config.AgentID,
); err != nil {
return nil, "", err return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Auggie")
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if err := requireAugmentAbsoluteNode(nodePath); err != nil {
return nil, "", err
}
if err := validateAugmentMatchers(document.hooks, nodePath); err != nil { if err := validateAugmentMatchers(document.hooks, nodePath); err != nil {
return nil, "", err return nil, "", err
} }
return paths, nodePath, nil 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( func prepareAugmentInstallationChanges(
config InstallConfig, config InstallConfig,
paths *augmentRuntimePaths, paths *augmentRuntimePaths,
@@ -156,63 +72,27 @@ func prepareAugmentInstallationChanges(
if paths == nil || rendered == nil { if paths == nil || rendered == nil {
return nil, fmt.Errorf("prepared Auggie installation is required") return nil, fmt.Errorf("prepared Auggie installation is required")
} }
if err := validateAugmentRuntimeIdentity( if err := validateAugmentRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil {
paths.agentDirectory,
config.AgentID,
); err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
items := []struct { for _, item := range []struct {
path, label string path, label string
content []byte content []byte
mode os.FileMode
}{ }{
{ {paths.guardWrapperPath, "Auggie guard wrapper", buildAugmentWrapper(nodePath, paths.guardPath)},
paths.guardPath, {paths.auditWrapperPath, "Auggie audit wrapper", buildAugmentWrapper(nodePath, paths.auditPath)},
"Elydora guard runtime", } {
[]byte(generateGuardScript( change, err := prepareFileChange(item.path, item.label, item.content, 0700)
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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
-44
View File
@@ -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 { func validateAugmentMatchers(hooks augmentHooks, nodePath string) error {
events := make([]string, 0, len(hooks)) events := make([]string, 0, len(hooks))
for event := range hooks { for event := range hooks {
@@ -209,10 +172,3 @@ func resolveAugmentWrapperPaths(agentDirectory string) augmentWrapperPaths {
audit: filepath.Join(agentDirectory, augmentAuditWrapperName()), 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
}
+26 -281
View File
@@ -2,11 +2,8 @@ package plugins
import ( import (
"bytes" "bytes"
"fmt"
"path/filepath" "path/filepath"
"runtime"
"sort" "sort"
"strings"
) )
func managedAugmentIDs( func managedAugmentIDs(
@@ -17,19 +14,10 @@ func managedAugmentIDs(
result := map[string]string{} result := map[string]string{}
for _, group := range groups { for _, group := range groups {
for _, handler := range group.handlers { for _, handler := range group.handlers {
agentID, managed := managedAugmentAgentID( agentID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
handler, if managed {
wrapperName, result[managedReferenceKey(agentID)] = agentID
runtimeRoot,
)
if !managed {
continue
} }
key := agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(key)
}
result[key] = agentID
} }
} }
return result return result
@@ -39,16 +27,8 @@ func augmentRuntimeContracts(
hooks augmentHooks, hooks augmentHooks,
runtimeRoot string, runtimeRoot string,
) []augmentRuntimeContract { ) []augmentRuntimeContract {
guards := managedAugmentIDs( guards := managedAugmentIDs(hooks["PreToolUse"], augmentGuardWrapperName(), runtimeRoot)
hooks["PreToolUse"], audits := managedAugmentIDs(hooks["PostToolUse"], augmentAuditWrapperName(), runtimeRoot)
augmentGuardWrapperName(),
runtimeRoot,
)
audits := managedAugmentIDs(
hooks["PostToolUse"],
augmentAuditWrapperName(),
runtimeRoot,
)
keys := make([]string, 0, len(guards)) keys := make([]string, 0, len(guards))
for key := range guards { for key := range guards {
if _, exists := audits[key]; exists { if _, exists := audits[key]; exists {
@@ -72,282 +52,47 @@ func augmentRuntimeContracts(
return contracts return contracts
} }
func readAugmentRuntimeConfig(path string) (map[string]any, bool, error) { func augmentWrappersMatch(contract augmentRuntimeContract, nodePath string) (bool, error) {
snapshot, err := readManagedFile( wrappers := resolveAugmentWrapperPaths(filepath.Dir(contract.guardPath))
path, if !sameManagedPath(contract.guardWrapper, wrappers.guard) ||
"Elydora runtime config", !sameManagedPath(contract.auditWrapper, wrappers.audit) {
maxRuntimeConfigBytes, return false, nil
)
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 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 { for _, item := range []struct {
path string path, label string
label string expected []byte
limit int64
}{ }{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes}, {contract.guardWrapper, "Auggie guard wrapper", buildAugmentWrapper(nodePath, contract.guardPath)},
{filepath.Join(agentDirectory, augmentGuardScript), "Elydora guard runtime", maxManagedSourceBytes}, {contract.auditWrapper, "Auggie audit wrapper", buildAugmentWrapper(nodePath, contract.auditPath)},
{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},
} { } {
exists, inspectErr := managedPhysicalFileExists( snapshot, err := readManagedFile(item.path, item.label, maxManagedSourceBytes)
item.path, if err != nil || snapshot == nil || !bytes.Equal(snapshot.contents, item.expected) {
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"] != 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 return false, err
} }
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return false, err
} }
configPath := filepath.Join(agentDirectory, "config.json") return true, nil
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
} }
func augmentRuntimeFilesExist( func augmentRuntimeFilesExist(
contracts []augmentRuntimeContract, contracts []augmentRuntimeContract,
runtimeRoot string, runtimeRoot string,
) (bool, error) { ) (bool, error) {
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Auggie")
if err != nil { if err != nil {
return false, err return false, err
} }
if err := requireAugmentAbsoluteNode(nodePath); err != nil {
return false, err
}
for _, contract := range contracts { for _, contract := range contracts {
exists, err := augmentRuntimeContractExists(contract, runtimeRoot, nodePath) runtime := managedRuntimeContract{contract.agentID, contract.guardPath, contract.auditPath}
exists, err := managedRuntimeContractExists(runtime, augmentAgentKey, "Auggie", nil)
if err != nil || !exists {
if err != nil { if err != nil {
return false, err return false, err
} }
if exists { continue
return true, nil }
matches, err := augmentWrappersMatch(contract, nodePath)
if err != nil || matches {
return matches, err
} }
} }
return false, nil return false, nil
@@ -54,7 +54,7 @@ func TestAugmentInstallRollsBackAllSevenFiles(t *testing.T) {
) )
failed := false failed := false
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameAugmentPath(destination, fixture.configPath) && if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") && strings.HasSuffix(source, ".tmp") &&
!failed { !failed {
failed = true failed = true
@@ -87,13 +87,14 @@ func TestPreparedAugmentInstallRejectsConcurrentSettingsChange(t *testing.T) {
if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil { if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil {
t.Fatalf("write concurrent settings: %v", err) t.Fatalf("write concurrent settings: %v", err)
} }
err := writeAugmentChanges( err := writeManagedChanges(
changes, changes,
"Install Augment Code CLI hooks", "Install Augment Code CLI hooks",
nil, nil,
paths.runtimeRoot, paths.runtimeRoot,
paths.agentDirectory, paths.agentDirectory,
filepath.Dir(document.configPath), filepath.Dir(document.configPath),
"Auggie configuration directory",
) )
if err == nil || !strings.Contains(err.Error(), "changed during installation") { if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("prepared install error = %v", err) t.Fatalf("prepared install error = %v", err)
@@ -375,7 +376,7 @@ func TestAugmentUninstallPreservesSettingsAfterCommitFailure(t *testing.T) {
t.Fatalf("read installed settings: %v", err) t.Fatalf("read installed settings: %v", err)
} }
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameAugmentPath(destination, fixture.configPath) && if sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
return errors.New("injected Auggie uninstall failure") return errors.New("injected Auggie uninstall failure")
} }
@@ -150,10 +150,10 @@ func TestClaudeValidatesManagedRuntimeInputsBeforeWrites(t *testing.T) {
}, "query parameters"}, }, "query parameters"},
{"guard path", func(f *claudeFixture) { {"guard path", func(f *claudeFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js") f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js")
}, "managed Elydora agent directory"}, }, "managed agent directory"},
{"audit path", func(f *claudeFixture) { {"audit path", func(f *claudeFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js") f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js")
}, "managed Elydora agent directory"}, }, "managed agent directory"},
} }
for _, testCase := range tests { for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
+1 -2
View File
@@ -7,8 +7,7 @@ type ClinePlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Cline commits both generated runtimes and // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// both native hook files in one transaction.
func (p *ClinePlugin) ManagesGuardRuntime() bool { func (p *ClinePlugin) ManagesGuardRuntime() bool {
return true return true
} }
+3 -42
View File
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
) )
@@ -80,37 +79,6 @@ func resolveClineHookFiles() (clineHookPaths, error) {
}, nil }, 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) { func buildClineMetadata(kind, agentID, runtimePath string) (clineHookMetadata, error) {
metadata := clineHookMetadata{ metadata := clineHookMetadata{
Version: clineMetadataVersion, Version: clineMetadataVersion,
@@ -314,7 +282,7 @@ func clineContractForFiles(
if guard.Kind != "guard" || audit.Kind != "audit" { if guard.Kind != "guard" || audit.Kind != "audit" {
return nil, fmt.Errorf("Elydora Cline hook files contain mismatched event metadata") 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") return nil, fmt.Errorf("Elydora Cline hook files reference different agents")
} }
agentDirectory, err := ResolveAgentRuntimeDirectory(guard.AgentID) agentDirectory, err := ResolveAgentRuntimeDirectory(guard.AgentID)
@@ -323,15 +291,8 @@ func clineContractForFiles(
} }
expectedGuard := filepath.Join(agentDirectory, clineGuardScript) expectedGuard := filepath.Join(agentDirectory, clineGuardScript)
expectedAudit := filepath.Join(agentDirectory, clineAuditScript) expectedAudit := filepath.Join(agentDirectory, clineAuditScript)
guardMatches, err := sameClinePath(guard.RuntimePath, expectedGuard) if !sameManagedPath(guard.RuntimePath, expectedGuard) ||
if err != nil { !sameManagedPath(audit.RuntimePath, expectedAudit) {
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 {
return nil, fmt.Errorf("Elydora Cline hook metadata references an unexpected runtime path") return nil, fmt.Errorf("Elydora Cline hook metadata references an unexpected runtime path")
} }
return &clineRuntimeContract{ return &clineRuntimeContract{
+20 -128
View File
@@ -1,97 +1,21 @@
package plugins package plugins
import ( import "fmt"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type clineRuntimePaths struct { type clineRuntimePaths struct {
runtimeRoot string managedRuntimePaths
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
hooks clineHookPaths hooks clineHookPaths
} }
func validateClineInstallConfig(config InstallConfig) error { func clineAgentPaths(config InstallConfig, hooks clineHookPaths) (*clineRuntimePaths, error) {
for _, field := range []struct{ name, value string }{ if err := validateManagedInstallConfig(config, clineAgentKey, "Cline"); err != nil {
{"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 {
return nil, err return nil, err
} }
runtimeRoot, err := AgentRuntimeRoot() paths, err := resolveManagedRuntimePaths(config, clineGuardScript, clineAuditScript)
if err != nil { if err != nil {
return nil, err return nil, err
} }
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID) return &clineRuntimePaths{managedRuntimePaths: *paths, hooks: hooks}, nil
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
} }
func preflightClineInstallation( func preflightClineInstallation(
@@ -102,39 +26,20 @@ func preflightClineInstallation(
if err != nil { if err != nil {
return nil, err 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 return nil, err
} }
nodePath, err := resolveNodeRuntime() if _, err := resolveAbsoluteNodeRuntime("Cline"); err != nil {
if err != nil {
return nil, err 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 return paths, nil
} }
func buildClineRuntimeConfig(config InstallConfig) ([]byte, error) { func clineExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
value := map[string]any{ return []byte(generateGuardScript(clineAgentKey, agentID, "", false, "")),
"org_id": config.OrgID, "agent_id": config.AgentID, "kid": config.KID, []byte(buildHookScriptWithOutput(clineAgentKey, agentID, "", false, true))
"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 prepareClineInstallationChanges( func prepareClineInstallationChanges(
@@ -146,11 +51,9 @@ func prepareClineInstallationChanges(
if paths == nil { if paths == nil {
return nil, fmt.Errorf("prepared Cline installation is required") return nil, fmt.Errorf("prepared Cline installation is required")
} }
if err := validateClineRuntimeIdentity(paths.agentDirectory, config.AgentID); err != nil { if err := validateManagedRuntimeIdentity(
return nil, err paths.agentDirectory, config.AgentID, clineAgentKey, "Cline",
} ); err != nil {
runtimeConfig, err := buildClineRuntimeConfig(config)
if err != nil {
return nil, err return nil, err
} }
guardMetadata, err := buildClineMetadata("guard", config.AgentID, paths.guardPath) guardMetadata, err := buildClineMetadata("guard", config.AgentID, paths.guardPath)
@@ -175,24 +78,13 @@ func prepareClineInstallationChanges(
); err != nil { ); err != nil {
return nil, err return nil, err
} }
runtimeItems := []struct { guardScript, auditScript := clineExpectedScripts(config.AgentID, nil)
path, label string changes, err := managedRuntimeFileChanges(
content []byte config, &paths.managedRuntimePaths, clineAgentKey, string(guardScript), string(auditScript),
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 { if err != nil {
return nil, err return nil, err
} }
changes = append(changes, change)
}
for _, item := range []struct { for _, item := range []struct {
file clineHookFile file clineHookFile
label string label string
+3 -12
View File
@@ -74,7 +74,7 @@ func writeClineHookChanges(
rename renameFunc, rename renameFunc,
paths clineHookPaths, paths clineHookPaths,
) error { ) error {
if !hasClineChanges(changes) { if !hasFileChanges(changes) {
return nil return nil
} }
if err := ensureManagedDirectory( if err := ensureManagedDirectory(
@@ -95,7 +95,7 @@ func writeClineChanges(
rename renameFunc, rename renameFunc,
paths *clineRuntimePaths, paths *clineRuntimePaths,
) error { ) error {
if !hasClineChanges(changes) { if !hasFileChanges(changes) {
return nil return nil
} }
if err := EnsurePrivateDirectory(paths.runtimeRoot); err != nil { if err := EnsurePrivateDirectory(paths.runtimeRoot); err != nil {
@@ -107,15 +107,6 @@ func writeClineChanges(
return writeClineHookChanges(changes, label, rename, paths.hooks) 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( func prepareClineUninstallChanges(
files []clineHookFile, files []clineHookFile,
agentID string, agentID string,
@@ -123,7 +114,7 @@ func prepareClineUninstallChanges(
changes := make([]*fileChange, 0, len(files)) changes := make([]*fileChange, 0, len(files))
for _, file := range files { for _, file := range files {
if file.metadata == nil || if file.metadata == nil ||
(agentID != "" && !sameClineAgentID(file.metadata.AgentID, agentID)) { (agentID != "" && !sameManagedAgentID(file.metadata.AgentID, agentID)) {
continue continue
} }
if err := assertClineWrapperIntegrity(file); err != nil { if err := assertClineWrapperIntegrity(file); err != nil {
+7 -198
View File
@@ -1,206 +1,15 @@
package plugins package plugins
import ( import "path/filepath"
"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
}
func clineRuntimeFilesExist(contract *clineRuntimeContract) (bool, error) { func clineRuntimeFilesExist(contract *clineRuntimeContract) (bool, error) {
runtimeRoot, err := AgentRuntimeRoot() if !sameManagedPath(contract.guardPath, filepath.Join(contract.agentDirectory, clineGuardScript)) {
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)) {
return false, nil return false, nil
} }
rootExists, err := managedPhysicalDirectoryExists(runtimeRoot, "Elydora runtime directory") return managedRuntimeContractExists(
if err != nil || !rootExists { managedRuntimeContract{contract.agentID, contract.guardPath, contract.auditPath},
return false, err clineAgentKey,
} "Cline",
directoryExists, err := managedPhysicalDirectoryExists( clineExpectedScripts,
contract.agentDirectory,
"Elydora agent runtime directory",
) )
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
} }
+3 -5
View File
@@ -10,14 +10,12 @@ type CopilotPlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Copilot commits its provider guard with the // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// audit runtime and hook documents.
func (p *CopilotPlugin) ManagesGuardRuntime() bool { func (p *CopilotPlugin) ManagesGuardRuntime() bool {
return true return true
} }
// PreflightInstall validates hook sources, settings, matchers, credentials, // PreflightInstall validates every source before the CLI creates runtime state.
// and runtime identity before any write.
func (p *CopilotPlugin) PreflightInstall(config InstallConfig) error { func (p *CopilotPlugin) PreflightInstall(config InstallConfig) error {
sources, _, err := readCopilotSources() sources, _, err := readCopilotSources()
if err != nil { if err != nil {
@@ -165,7 +163,7 @@ func mergedCopilotContracts(
} }
unique := map[string]copilotRuntimeContract{} unique := map[string]copilotRuntimeContract{}
for _, contract := range contracts { for _, contract := range contracts {
unique[copilotEntryKey(contract.agentID)] = contract unique[managedReferenceKey(contract.agentID)] = contract
} }
result := make([]copilotRuntimeContract, 0, len(unique)) result := make([]copilotRuntimeContract, 0, len(unique))
for _, contract := range unique { for _, contract := range unique {
+21 -149
View File
@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"path/filepath" "path/filepath"
"runtime"
"sort" "sort"
"strings" "strings"
) )
@@ -17,7 +16,6 @@ const (
copilotConfigFile = "elydora-audit.json" copilotConfigFile = "elydora-audit.json"
copilotHookTimeout = float64(10) copilotHookTimeout = float64(10)
copilotLegacyTimeout = float64(5) copilotLegacyTimeout = float64(5)
copilotPOSIXApostrophe = `'"'"'`
) )
var copilotManagedEvents = []struct { var copilotManagedEvents = []struct {
@@ -61,15 +59,19 @@ type copilotRenderedDocument struct {
remove bool remove bool
} }
type copilotRuntimeContract struct { type copilotRuntimeContract = managedRuntimeContract
agentID string
guardPath string
auditPath string
}
type copilotManagedEntry struct { type copilotManagedEntry = managedScriptReference
agentID string
scriptPath string 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 { func cloneCopilotObject(value map[string]any) map[string]any {
@@ -90,109 +92,15 @@ func cloneCopilotHooks(source copilotHooks) copilotHooks {
return clone 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 { func buildCopilotHandler(nodePath, scriptPath string) map[string]any {
return map[string]any{ return map[string]any{
"type": "command", "type": "command",
"bash": quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath), "bash": posixSource(nodePath, scriptPath),
"powershell": "& " + quoteCopilotPowerShell(nodePath) + " " + quoteCopilotPowerShell(scriptPath) + "; exit $LASTEXITCODE", "powershell": powerShellSource(nodePath, scriptPath),
"timeoutSec": copilotHookTimeout, "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) { func parseCopilotLegacyCommand(command string) (string, bool) {
command = strings.TrimSpace(command) command = strings.TrimSpace(command)
separator := strings.IndexAny(command, " \t") separator := strings.IndexAny(command, " \t")
@@ -211,11 +119,6 @@ func parseCopilotLegacyCommand(command string) (string, bool) {
return script, script != "" 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) { func copilotManagedScriptPath(handler map[string]any) (string, bool) {
if len(handler) != 4 || handler["type"] != "command" { if len(handler) != 4 || handler["type"] != "command" {
return "", false return "", false
@@ -226,11 +129,11 @@ func copilotManagedScriptPath(handler map[string]any) (string, bool) {
return "", false return "", false
} }
if handler["timeoutSec"] == copilotHookTimeout { if handler["timeoutSec"] == copilotHookTimeout {
bashNode, bashScript, bashParsed := parseCopilotBash(bash) bashNode, bashScript, bashParsed := parsePOSIXCommand(bash)
psNode, psScript, psParsed := parseCopilotPowerShell(powershell) psNode, psScript, psParsed := parsePowerShellSource(powershell)
if bashParsed && psParsed && filepath.IsAbs(bashNode) && filepath.IsAbs(bashScript) && if bashParsed && psParsed && filepath.IsAbs(bashNode) && filepath.IsAbs(bashScript) &&
isCopilotNodeExecutable(bashNode) && sameCopilotPath(bashNode, psNode) && isNodeExecutable(bashNode) && sameManagedPath(bashNode, psNode) &&
sameCopilotPath(bashScript, psScript) { sameManagedPath(bashScript, psScript) {
return bashScript, true return bashScript, true
} }
} }
@@ -240,30 +143,6 @@ func copilotManagedScriptPath(handler map[string]any) (string, bool) {
return parseCopilotLegacyCommand(bash) 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( func removeManagedCopilotHooks(
hooks copilotHooks, hooks copilotHooks,
agentID string, agentID string,
@@ -277,7 +156,7 @@ func removeManagedCopilotHooks(
return nil, err return nil, err
} }
remove := entry != nil && remove := entry != nil &&
(agentID == "" || sameCopilotAgentID(entry.agentID, agentID)) (agentID == "" || sameManagedAgentID(entry.agentID, agentID))
if !remove { if !remove {
kept = append(kept, handler) kept = append(kept, handler)
} }
@@ -387,13 +266,6 @@ func renderCopilotDocument(
}, nil }, nil
} }
func copilotEntryKey(agentID string) string {
if runtime.GOOS == "windows" {
return strings.ToLower(agentID)
}
return agentID
}
func copilotManagedEntries( func copilotManagedEntries(
handlers []map[string]any, handlers []map[string]any,
scriptName string, scriptName string,
@@ -405,7 +277,7 @@ func copilotManagedEntries(
return nil, err return nil, err
} }
if entry != nil { if entry != nil {
key := copilotEntryKey(entry.agentID) key := managedReferenceKey(entry.agentID)
result[key] = append(result[key], *entry) result[key] = append(result[key], *entry)
} }
} }
@@ -438,7 +310,7 @@ func copilotRuntimeContracts(hooks copilotHooks) ([]copilotRuntimeContract, erro
success := successes[key] success := successes[key]
failure := failures[key] failure := failures[key]
if len(guard) != 1 || len(success) != 1 || len(failure) != 1 || 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 continue
} }
contracts = append(contracts, copilotRuntimeContract{ contracts = append(contracts, copilotRuntimeContract{
+20 -142
View File
@@ -1,103 +1,19 @@
package plugins package plugins
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings"
) )
type copilotRuntimePaths struct {
runtimeRoot string
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
}
type preparedCopilotInstallation struct { type preparedCopilotInstallation struct {
changes []*fileChange changes []*fileChange
preconditions []filePrecondition preconditions []filePrecondition
paths *copilotRuntimePaths paths *managedRuntimePaths
}
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
} }
func preflightCopilotInstallation( func preflightCopilotInstallation(
config InstallConfig, config InstallConfig,
sources *copilotSources, sources *copilotSources,
) (*copilotRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if sources == nil || sources.user == nil || sources.user.filePath == "" { if sources == nil || sources.user == nil || sources.user.filePath == "" {
return nil, "", fmt.Errorf( return nil, "", fmt.Errorf(
"GitHub Copilot CLI installation requires a user hook path", "GitHub Copilot CLI installation requires a user hook path",
@@ -106,22 +22,22 @@ func preflightCopilotInstallation(
if err := requireCopilotHooksEnabled(sources); err != nil { if err := requireCopilotHooksEnabled(sources); err != nil {
return nil, "", err 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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("GitHub Copilot CLI")
if err != nil { if err != nil {
return nil, "", err 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} hookSources := []copilotHooks{sources.user.hooks}
if sources.legacy != nil { if sources.legacy != nil {
hookSources = append(hookSources, sources.legacy.hooks) hookSources = append(hookSources, sources.legacy.hooks)
@@ -132,28 +48,6 @@ func preflightCopilotInstallation(
return paths, nodePath, nil 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( func validateCopilotRenderedSet(
sources *copilotSources, sources *copilotSources,
rendered []*copilotRenderedDocument, rendered []*copilotRenderedDocument,
@@ -169,7 +63,7 @@ func validateCopilotRenderedSet(
matches := 0 matches := 0
for _, item := range rendered { for _, item := range rendered {
if item != nil && item.document != nil && if item != nil && item.document != nil &&
sameCopilotPath(item.document.filePath, document.filePath) { sameManagedPath(item.document.filePath, document.filePath) {
matches++ matches++
} }
} }
@@ -218,34 +112,13 @@ func prepareCopilotInstallation(
if err := validateCopilotRenderedSet(sources, rendered); err != nil { if err := validateCopilotRenderedSet(sources, rendered); err != nil {
return nil, err 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 { if err != nil {
return nil, err 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 { for _, document := range rendered {
change, changeErr := prepareRenderedCopilotChange(document) change, changeErr := prepareRenderedCopilotChange(document)
if changeErr != nil { if changeErr != nil {
@@ -275,3 +148,8 @@ func commitCopilotInstallation(
prepared.preconditions, prepared.preconditions,
) )
} }
func copilotExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(copilotAgentKey, agentID, "", false, "")),
[]byte(buildHookScriptWithOutput(copilotAgentKey, agentID, "", false, true))
}
+1 -8
View File
@@ -240,14 +240,7 @@ func writeCopilotChanges(
agentDirectory string, agentDirectory string,
preconditions []filePrecondition, preconditions []filePrecondition,
) error { ) error {
hasChanges := false if !hasFileChanges(changes) {
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
return writeChanges(changes, label, rename, preconditions...) return writeChanges(changes, label, rename, preconditions...)
} }
if agentDirectory != "" { if agentDirectory != "" {
+2 -258
View File
@@ -1,261 +1,5 @@
package plugins package plugins
import ( func copilotRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
"bytes" return managedRuntimeFilesExist(contracts, copilotAgentKey, "Copilot", copilotExpectedScripts)
"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
} }
+1 -1
View File
@@ -342,7 +342,7 @@ func TestCursorCreatesMissingGuardAndRejectsUnmanagedRuntimePaths(t *testing.T)
fixture.config.HookScript = unmanaged fixture.config.HookScript = unmanaged
} }
if err := fixture.plugin.Install(fixture.config); err == nil || 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) t.Fatalf("unmanaged %s error = %v", field, err)
} }
if _, err := os.Stat(fixture.configPath); !os.IsNotExist(err) { if _, err := os.Stat(fixture.configPath); !os.IsNotExist(err) {
+4 -6
View File
@@ -7,14 +7,12 @@ type DroidPlugin struct {
rename renameFunc rename renameFunc
} }
// ManagesGuardRuntime reports that Droid commits its guard, audit runtime, // ManagesGuardRuntime reports that the guard is committed with the audit runtime.
// credentials, and hook documents in one transaction.
func (p *DroidPlugin) ManagesGuardRuntime() bool { func (p *DroidPlugin) ManagesGuardRuntime() bool {
return true return true
} }
// PreflightInstall validates every hook source, policy layer, credential, // PreflightInstall validates every source before the CLI creates runtime state.
// matcher, and runtime identity before any write.
func (p *DroidPlugin) PreflightInstall(config InstallConfig) error { func (p *DroidPlugin) PreflightInstall(config InstallConfig) error {
sources, err := readDroidSources() sources, err := readDroidSources()
if err != nil { if err != nil {
@@ -107,7 +105,7 @@ func (p *DroidPlugin) Uninstall(agentID string) error {
if err != nil { if err != nil {
return err return err
} }
runtimeRoot, err := droidRuntimeRoot() runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
return err return err
} }
@@ -133,7 +131,7 @@ func (p *DroidPlugin) Status() (PluginStatus, error) {
if droidHookBlocked(sources) != nil { if droidHookBlocked(sources) != nil {
return status, nil return status, nil
} }
runtimeRoot, err := droidRuntimeRoot() runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
return status, err return status, err
} }
+1 -1
View File
@@ -239,7 +239,7 @@ func droidAdditionsFor(
document, target *droidDocument, document, target *droidDocument,
groups map[string]map[string]any, groups map[string]map[string]any,
) 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 groups
} }
return map[string]map[string]any{} return map[string]map[string]any{}
+9 -119
View File
@@ -20,8 +20,6 @@ const (
droidAuditScript = "hook.js" droidAuditScript = "hook.js"
droidHookTimeout = float64(10) droidHookTimeout = float64(10)
droidOwnedFileMarker = "// Managed by Elydora" droidOwnedFileMarker = "// Managed by Elydora"
droidPOSIXApostrophe = `'"'"'`
droidWindowsExitSuffix = "; exit $LASTEXITCODE"
) )
var droidToolEvents = [...]string{"PreToolUse", "PostToolUse"} var droidToolEvents = [...]string{"PreToolUse", "PostToolUse"}
@@ -35,11 +33,7 @@ type droidManagedRemoval struct {
removeGroup bool removeGroup bool
} }
type droidRuntimeContract struct { type droidRuntimeContract = managedRuntimeContract
agentID string
guardPath string
auditPath string
}
type droidRegexEntry struct { type droidRegexEntry struct {
Label string `json:"label"` Label string `json:"label"`
@@ -180,14 +174,9 @@ for (const entry of entries) {
func buildDroidCommand(nodePath, scriptPath string) string { func buildDroidCommand(nodePath, scriptPath string) string {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return "& " + quoteDroidPowerShellArgument(nodePath) + " " + return powerShellSource(nodePath, scriptPath)
quoteDroidPowerShellArgument(scriptPath) + droidWindowsExitSuffix
} }
return quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath) return posixSource(nodePath, scriptPath)
}
func quoteDroidPowerShellArgument(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
} }
func buildDroidGroup(nodePath, scriptPath string) map[string]any { 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) { func parseDroidCommand(command string, includeLegacy ...bool) (string, string, bool) {
if runtime.GOOS != "windows" { 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] { if ok || len(includeLegacy) == 0 || !includeLegacy[0] {
return executable, script, ok return executable, script, ok
} }
return parseDroidLegacyWindowsCommand(command) 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) { func parseDroidLegacyWindowsCommand(command string) (string, string, bool) {
executable, next, ok := readDroidLegacyWindowsArgument(command, 0) executable, next, ok := readDroidLegacyWindowsArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' { 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 != "" 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) { func readDroidLegacyWindowsArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '"' { if start >= len(command) || command[start] != '"' {
return "", start, false 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") 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( func managedDroidAgentID(
handler map[string]any, handler map[string]any,
scriptName, runtimeRoot string, scriptName, runtimeRoot string,
@@ -333,29 +235,17 @@ func managedDroidAgentID(
} }
executable, scriptPath, ok := parseDroidCommand(command, includeLegacy...) executable, scriptPath, ok := parseDroidCommand(command, includeLegacy...)
if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) || if !ok || !filepath.IsAbs(executable) || !filepath.IsAbs(scriptPath) ||
!isDroidNodeExecutable(executable) || !sameDroidFileName(filepath.Base(scriptPath), scriptName) { !isNodeExecutable(executable) || !sameManagedName(filepath.Base(scriptPath), scriptName) {
return "", false return "", false
} }
agentDirectory := filepath.Dir(scriptPath) agentDirectory := filepath.Dir(scriptPath)
if !sameDroidPath(filepath.Dir(agentDirectory), runtimeRoot) { if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false return "", false
} }
agentID := filepath.Base(agentDirectory) agentID := filepath.Base(agentDirectory)
return agentID, agentID != "" && agentID != "." && agentID != ".." 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 { func managedDroidRemovals(settings droidHookSettings, agentID, runtimeRoot string) []droidManagedRemoval {
removals := make([]droidManagedRemoval, 0) removals := make([]droidManagedRemoval, 0)
for _, contract := range []struct{ event, script string }{ for _, contract := range []struct{ event, script string }{
@@ -370,7 +260,7 @@ func managedDroidRemovals(settings droidHookSettings, agentID, runtimeRoot strin
managedID, managed := managedDroidAgentID( managedID, managed := managedDroidAgentID(
handlerValue.(map[string]any), contract.script, runtimeRoot, true, handlerValue.(map[string]any), contract.script, runtimeRoot, true,
) )
if managed && (agentID == "" || sameDroidAgentID(managedID, agentID)) { if managed && (agentID == "" || sameManagedAgentID(managedID, agentID)) {
indexes = append(indexes, handlerIndex) indexes = append(indexes, handlerIndex)
} }
} }
@@ -400,7 +290,7 @@ func droidRuntimeContracts(settings droidHookSettings, runtimeRoot string) []dro
contracts := make([]droidRuntimeContract, 0, len(guardIDs)) contracts := make([]droidRuntimeContract, 0, len(guardIDs))
for _, guardID := range guardIDs { for _, guardID := range guardIDs {
for auditID := range audits { for auditID := range audits {
if !sameDroidAgentID(guardID, auditID) { if !sameManagedAgentID(guardID, auditID) {
continue continue
} }
root := filepath.Join(runtimeRoot, guardID) root := filepath.Join(runtimeRoot, guardID)
+23 -156
View File
@@ -1,24 +1,13 @@
package plugins package plugins
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings"
) )
type droidRuntimePaths struct {
runtimeRoot string
agentDirectory string
guardPath string
auditPath string
}
type preparedDroidInstallation struct { type preparedDroidInstallation struct {
changes []*fileChange changes []*fileChange
preconditions []filePrecondition preconditions []filePrecondition
paths *droidRuntimePaths paths *managedRuntimePaths
} }
type preparedDroidUninstall struct { 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( func preflightDroidInstallation(
config InstallConfig, config InstallConfig,
sources *droidSources, sources *droidSources,
) (*droidRuntimePaths, string, error) { ) (*managedRuntimePaths, string, error) {
if sources == nil || sources.root == nil || sources.policy == nil { if sources == nil || sources.root == nil || sources.policy == nil {
return nil, "", fmt.Errorf("Factory Droid installation sources are required") return nil, "", fmt.Errorf("Factory Droid installation sources are required")
} }
@@ -126,50 +45,25 @@ func preflightDroidInstallation(
if err := validateDroidRegexes(hooks...); err != nil { if err := validateDroidRegexes(hooks...); err != nil {
return nil, "", err 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 { if err != nil {
return nil, "", err 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 return nil, "", err
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveAbsoluteNodeRuntime("Factory Droid")
if err != nil { if err != nil {
return nil, "", err 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 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( func prepareRenderedDroidChange(
rendered *droidRenderedDocument, rendered *droidRenderedDocument,
) (*fileChange, error) { ) (*fileChange, error) {
@@ -198,7 +92,7 @@ func validateDroidRenderedSet(
matches := 0 matches := 0
for _, item := range rendered { for _, item := range rendered {
if item != nil && item.document != nil && if item != nil && item.document != nil &&
sameDroidPath(item.document.filePath, document.filePath) { sameManagedPath(item.document.filePath, document.filePath) {
matches++ matches++
} }
} }
@@ -217,7 +111,7 @@ func droidSourcePreconditions(
for _, document := range droidSourceDocuments(sources) { for _, document := range droidSourceDocuments(sources) {
changed := false changed := false
for _, path := range changedPaths { for _, path := range changedPaths {
if sameDroidPath(document.filePath, path) { if sameManagedPath(document.filePath, path) {
changed = true changed = true
break break
} }
@@ -246,38 +140,13 @@ func prepareDroidInstallation(
if err := validateDroidRenderedSet(sources, rendered); err != nil { if err := validateDroidRenderedSet(sources, rendered); err != nil {
return nil, err 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 { if err != nil {
return nil, err 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)) changedPaths := make([]string, 0, len(rendered))
for _, document := range rendered { for _, document := range rendered {
change, changeErr := prepareRenderedDroidChange(document) change, changeErr := prepareRenderedDroidChange(document)
@@ -305,14 +174,7 @@ func writeDroidChanges(
runtimeRoot, agentDirectory string, runtimeRoot, agentDirectory string,
preconditions []filePrecondition, preconditions []filePrecondition,
) error { ) error {
hasChanges := false if !hasFileChanges(changes) {
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
return writeChanges(changes, label, rename, preconditions...) return writeChanges(changes, label, rename, preconditions...)
} }
if err := assertFilePreconditions(preconditions, label); err != nil { if err := assertFilePreconditions(preconditions, label); err != nil {
@@ -365,7 +227,7 @@ func prepareDroidUninstall(
for _, item := range rendered { for _, item := range rendered {
changed := false changed := false
for _, path := range changedPaths { for _, path := range changedPaths {
if sameDroidPath(item.document.filePath, path) { if sameManagedPath(item.document.filePath, path) {
changed = true changed = true
break break
} }
@@ -398,3 +260,8 @@ func commitDroidUninstall(
prepared.preconditions, prepared.preconditions,
) )
} }
func droidExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
return []byte(generateGuardScript(droidAgentKey, agentID, "", false, "")),
[]byte(buildHookScriptWithOutput(droidAgentKey, agentID, "", false, true))
}
+2 -248
View File
@@ -1,251 +1,5 @@
package plugins package plugins
import ( func droidRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
"bytes" return managedRuntimeFilesExist(contracts, droidAgentKey, "Factory Droid", droidExpectedScripts)
"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
} }
+1 -1
View File
@@ -57,7 +57,7 @@ func TestDroidInstallWritesCurrentContainerAndCompleteRuntime(t *testing.T) {
requireDroidNativeGroup(t, auditGroup) requireDroidNativeGroup(t, auditGroup)
guardCommand := droidManagedHandler(t, hooks["PreToolUse"], fixture.guardPath)["command"].(string) guardCommand := droidManagedHandler(t, hooks["PreToolUse"], fixture.guardPath)["command"].(string)
if runtime.GOOS == "windows" && 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) t.Fatalf("Windows Factory command = %q", guardCommand)
} }
runtimeConfig := readDroidTestObject(t, fixture.runtimeConfig) runtimeConfig := readDroidTestObject(t, fixture.runtimeConfig)
@@ -237,7 +237,7 @@ func droidManagedGroup(t *testing.T, groupsValue any, scriptPath string) map[str
handler := requireDroidObject(t, handlerValue) handler := requireDroidObject(t, handlerValue)
command, _ := handler["command"].(string) command, _ := handler["command"].(string)
_, configuredPath, managed := parseDroidCommand(command, true) _, configuredPath, managed := parseDroidCommand(command, true)
if managed && sameDroidPath(configuredPath, scriptPath) { if managed && sameManagedPath(configuredPath, scriptPath) {
return group return group
} }
} }
@@ -253,7 +253,7 @@ func droidManagedHandler(t *testing.T, groupsValue any, scriptPath string) map[s
handler := requireDroidObject(t, handlerValue) handler := requireDroidObject(t, handlerValue)
command, _ := handler["command"].(string) command, _ := handler["command"].(string)
_, configuredPath, managed := parseDroidCommand(command, true) _, configuredPath, managed := parseDroidCommand(command, true)
if managed && sameDroidPath(configuredPath, scriptPath) { if managed && sameManagedPath(configuredPath, scriptPath) {
return handler return handler
} }
} }
@@ -53,7 +53,7 @@ func TestDroidInstallRollsBackRuntimeAfterLateHookCommitFailure(t *testing.T) {
prepared := prepareDroidTestInstallation(t, fixture, fixture.config) prepared := prepareDroidTestInstallation(t, fixture, fixture.config)
failed := false failed := false
rename := func(source, destination string) error { rename := func(source, destination string) error {
if !failed && sameDroidPath(destination, fixture.configPath) { if !failed && sameManagedPath(destination, fixture.configPath) {
failed = true failed = true
return errors.New("injected Droid hook commit failure") return errors.New("injected Droid hook commit failure")
} }
@@ -85,7 +85,7 @@ func TestDroidConcurrentActiveHookReplacementPreventsRuntimeCommit(t *testing.T)
t.Fatalf("concurrent source error = %v", err) t.Fatalf("concurrent source error = %v", err)
} }
for path, source := range before { for path, source := range before {
if sameDroidPath(path, fixture.configPath) { if sameManagedPath(path, fixture.configPath) {
continue continue
} }
if readDroidTestFile(t, path) != source { if readDroidTestFile(t, path) != source {
@@ -205,7 +205,7 @@ func TestDroidSameContentReplacementIsRejectedByPhysicalIdentity(t *testing.T) {
t.Fatal("same-content replacement changed") t.Fatal("same-content replacement changed")
} }
for path, source := range before { 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) t.Fatalf("runtime changed after inode replacement: %s", path)
} }
} }
@@ -226,7 +226,7 @@ func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("read uninstall sources: %v", err) t.Fatalf("read uninstall sources: %v", err)
} }
runtimeRoot, err := droidRuntimeRoot() runtimeRoot, err := AgentRuntimeRoot()
if err != nil { if err != nil {
t.Fatalf("resolve runtime root: %v", err) t.Fatalf("resolve runtime root: %v", err)
} }
@@ -240,7 +240,7 @@ func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) {
} }
failed := false failed := false
rename := func(source, destination string) error { rename := func(source, destination string) error {
if !failed && sameDroidPath(destination, fixture.settingsPath) { if !failed && sameManagedPath(destination, fixture.settingsPath) {
failed = true failed = true
return errors.New("injected Droid uninstall failure") return errors.New("injected Droid uninstall failure")
} }
+2 -2
View File
@@ -164,10 +164,10 @@ func TestGeminiValidatesManagedRuntimeInputsBeforeWrites(t *testing.T) {
}, "query parameters"}, }, "query parameters"},
{"guard path", func(f *geminiFixture) { {"guard path", func(f *geminiFixture) {
f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js") f.config.GuardScriptPath = filepath.Join(f.homeDir, "outside", "guard.js")
}, "managed Elydora agent directory"}, }, "managed agent directory"},
{"audit path", func(f *geminiFixture) { {"audit path", func(f *geminiFixture) {
f.config.HookScript = filepath.Join(f.homeDir, "outside", "hook.js") 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) { t.Run(testCase.name, func(t *testing.T) {
fixture := prepareGeminiFixture(t, geminiFixtureOptions{}) fixture := prepareGeminiFixture(t, geminiFixtureOptions{})
+2 -2
View File
@@ -136,14 +136,14 @@ func resolveManagedRuntimePaths(
if !filepath.IsAbs(config.GuardScriptPath) || if !filepath.IsAbs(config.GuardScriptPath) ||
!sameManagedPath(config.GuardScriptPath, paths.guardPath) { !sameManagedPath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf( 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, paths.guardPath,
) )
} }
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) || if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameManagedPath(config.HookScript, paths.auditPath)) { !sameManagedPath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf( 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, paths.auditPath,
) )
} }