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:
1 parent
a7fc75bf6b
commit
1ede03d989
28 files changed
+234
-2051
No files matched your search
@@ -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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
augmentAgentKey = "augment"
|
augmentAgentKey = "augment"
|
||||||
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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
guardWrapperPath: wrappers.guard,
|
||||||
wrappers := resolveAugmentWrapperPaths(agentDirectory)
|
auditWrapperPath: wrappers.audit,
|
||||||
paths := &augmentRuntimePaths{
|
}, nil
|
||||||
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
|
}
|
||||||
configPath: filepath.Join(agentDirectory, "config.json"),
|
|
||||||
keyPath: filepath.Join(agentDirectory, "private.key"),
|
func validateAugmentRuntimeIdentity(agentDirectory, agentID string) error {
|
||||||
guardPath: filepath.Join(agentDirectory, augmentGuardScript),
|
return validateManagedRuntimeIdentity(
|
||||||
auditPath: filepath.Join(agentDirectory, augmentAuditScript),
|
agentDirectory, agentID, augmentAgentKey, "Auggie",
|
||||||
guardWrapperPath: wrappers.guard,
|
augmentWrapperArtifacts(agentDirectory)...,
|
||||||
auditWrapperPath: wrappers.audit,
|
)
|
||||||
}
|
|
||||||
if !filepath.IsAbs(config.GuardScriptPath) ||
|
|
||||||
!sameAugmentPath(config.GuardScriptPath, paths.guardPath) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"Elydora guard runtime must use the managed agent directory: %s",
|
|
||||||
paths.guardPath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
|
|
||||||
!sameAugmentPath(config.HookScript, paths.auditPath)) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"Elydora audit runtime must use the managed agent directory: %s",
|
|
||||||
paths.auditPath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return paths, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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,
|
return false, err
|
||||||
item.limit,
|
|
||||||
)
|
|
||||||
if inspectErr != nil {
|
|
||||||
return inspectErr
|
|
||||||
}
|
}
|
||||||
artifactExists = artifactExists || exists
|
|
||||||
}
|
}
|
||||||
if !configExists {
|
return true, nil
|
||||||
if artifactExists {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"Elydora runtime identity cannot be verified without config.json: %s",
|
|
||||||
agentDirectory,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
configuredID, ok := config["agent_id"].(string)
|
|
||||||
if !ok || config["agent_name"] != augmentAgentKey ||
|
|
||||||
!sameAugmentAgentID(configuredID, agentID) {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"Elydora runtime config identity does not match Auggie agent %s: %s",
|
|
||||||
agentID,
|
|
||||||
configPath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func augmentRuntimeContractExists(
|
|
||||||
contract augmentRuntimeContract,
|
|
||||||
runtimeRoot string,
|
|
||||||
nodePath string,
|
|
||||||
) (bool, error) {
|
|
||||||
agentDirectory := filepath.Dir(contract.guardPath)
|
|
||||||
wrappers := resolveAugmentWrapperPaths(agentDirectory)
|
|
||||||
if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) ||
|
|
||||||
!sameAugmentPath(
|
|
||||||
contract.auditPath,
|
|
||||||
filepath.Join(agentDirectory, augmentAuditScript),
|
|
||||||
) ||
|
|
||||||
!sameAugmentPath(contract.guardWrapper, wrappers.guard) ||
|
|
||||||
!sameAugmentPath(contract.auditWrapper, wrappers.audit) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
rootExists, err := managedPhysicalDirectoryExists(
|
|
||||||
runtimeRoot,
|
|
||||||
"Elydora runtime directory",
|
|
||||||
)
|
|
||||||
if err != nil || !rootExists {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
directoryExists, err := managedPhysicalDirectoryExists(
|
|
||||||
agentDirectory,
|
|
||||||
"Elydora agent runtime directory",
|
|
||||||
)
|
|
||||||
if err != nil || !directoryExists {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
configPath := filepath.Join(agentDirectory, "config.json")
|
|
||||||
keyPath := filepath.Join(agentDirectory, "private.key")
|
|
||||||
config, configExists, err := readAugmentRuntimeConfig(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
guard, err := readManagedFile(
|
|
||||||
contract.guardPath,
|
|
||||||
"Elydora guard runtime",
|
|
||||||
maxManagedSourceBytes,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
audit, err := readManagedFile(
|
|
||||||
contract.auditPath,
|
|
||||||
"Elydora audit runtime",
|
|
||||||
maxManagedSourceBytes,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
guardWrapper, err := readManagedFile(
|
|
||||||
contract.guardWrapper,
|
|
||||||
"Auggie guard wrapper",
|
|
||||||
maxManagedSourceBytes,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
auditWrapper, err := readManagedFile(
|
|
||||||
contract.auditWrapper,
|
|
||||||
"Auggie audit wrapper",
|
|
||||||
maxManagedSourceBytes,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if !configExists || key == nil || guard == nil || audit == nil ||
|
|
||||||
guardWrapper == nil || auditWrapper == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if err := validateAugmentRuntimeConfig(
|
|
||||||
config,
|
|
||||||
contract.agentID,
|
|
||||||
configPath,
|
|
||||||
); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := validateManagedPrivateKey(string(key.contents)); err != nil {
|
|
||||||
return false, fmt.Errorf("Elydora private key at %s: %w", keyPath, err)
|
|
||||||
}
|
|
||||||
return len(guard.contents) > 0 &&
|
|
||||||
len(audit.contents) > 0 &&
|
|
||||||
bytes.Equal(
|
|
||||||
guardWrapper.contents,
|
|
||||||
buildAugmentWrapper(nodePath, contract.guardPath),
|
|
||||||
) &&
|
|
||||||
bytes.Equal(
|
|
||||||
auditWrapper.contents,
|
|
||||||
buildAugmentWrapper(nodePath, contract.auditPath),
|
|
||||||
), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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}
|
||||||
if err != nil {
|
exists, err := managedRuntimeContractExists(runtime, augmentAgentKey, "Auggie", nil)
|
||||||
return false, err
|
if err != nil || !exists {
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if exists {
|
matches, err := augmentWrappersMatch(contract, nodePath)
|
||||||
return true, nil
|
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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
hooks clineHookPaths
|
||||||
configPath string
|
|
||||||
keyPath string
|
|
||||||
guardPath string
|
|
||||||
auditPath string
|
|
||||||
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,23 +78,12 @@ 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
|
)
|
||||||
}{
|
if err != nil {
|
||||||
{paths.guardPath, "Elydora guard runtime", []byte(generateGuardScript(clineAgentKey, config.AgentID, "", false, "")), 0700},
|
return nil, err
|
||||||
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600},
|
|
||||||
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600},
|
|
||||||
{paths.auditPath, "Elydora audit runtime", []byte(buildHookScriptWithOutput(clineAgentKey, config.AgentID, "", false, true)), 0700},
|
|
||||||
}
|
|
||||||
changes := make([]*fileChange, 0, len(runtimeItems)+2)
|
|
||||||
for _, item := range runtimeItems {
|
|
||||||
change, err := prepareFileChange(item.path, item.label, item.content, item.mode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
changes = append(changes, change)
|
|
||||||
}
|
}
|
||||||
for _, item := range []struct {
|
for _, item := range []struct {
|
||||||
file clineHookFile
|
file clineHookFile
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -5,19 +5,17 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
copilotAgentKey = "copilot"
|
copilotAgentKey = "copilot"
|
||||||
copilotGuardScript = "guard.js"
|
copilotGuardScript = "guard.js"
|
||||||
copilotAuditScript = "hook.js"
|
copilotAuditScript = "hook.js"
|
||||||
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{
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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 != "" {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -15,13 +15,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
droidAgentKey = "droid"
|
droidAgentKey = "droid"
|
||||||
droidGuardScript = "guard.js"
|
droidGuardScript = "guard.js"
|
||||||
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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user