feat: add native Factory Droid hook integration
This commit is contained in:
1 parent
2eff1ae119
commit
577e3609b5
13 files changed
+2292
-1
No files matched your search
@@ -0,0 +1,329 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type droidRenameFunc func(source, destination string) error
|
||||
|
||||
type droidFileChange struct {
|
||||
filePath string
|
||||
label string
|
||||
original []byte
|
||||
originalMode os.FileMode
|
||||
existed bool
|
||||
next []byte
|
||||
mode os.FileMode
|
||||
remove bool
|
||||
}
|
||||
|
||||
type droidStagedChange struct {
|
||||
change droidFileChange
|
||||
temporaryPath string
|
||||
rollbackPath string
|
||||
committed bool
|
||||
}
|
||||
|
||||
func prepareDroidFileChange(
|
||||
filePath, label string,
|
||||
next []byte,
|
||||
mode os.FileMode,
|
||||
) (*droidFileChange, error) {
|
||||
original, exists, err := readDroidOptional(filePath, label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists && bytes.Equal(original, next) {
|
||||
return nil, nil
|
||||
}
|
||||
if !exists && next == nil {
|
||||
return nil, nil
|
||||
}
|
||||
originalMode := mode
|
||||
if exists {
|
||||
info, statErr := os.Stat(filePath)
|
||||
if statErr != nil {
|
||||
return nil, fmt.Errorf("read file mode for %s at %s: %w", label, filePath, statErr)
|
||||
}
|
||||
originalMode = info.Mode().Perm()
|
||||
}
|
||||
return &droidFileChange{
|
||||
filePath: filePath, label: label, original: original, originalMode: originalMode,
|
||||
existed: exists, next: append([]byte(nil), next...), mode: mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func prepareRenderedDroidChange(rendered *droidRenderedDocument) (*droidFileChange, error) {
|
||||
if rendered == nil || !rendered.changed {
|
||||
return nil, nil
|
||||
}
|
||||
label := "Factory Droid hooks"
|
||||
if rendered.document.kind == "legacy" {
|
||||
label = "Factory Droid legacy hooks"
|
||||
} else if rendered.document.kind == "settings" {
|
||||
label = "Factory Droid settings"
|
||||
}
|
||||
originalMode := os.FileMode(0600)
|
||||
var original []byte
|
||||
if rendered.document.exists {
|
||||
info, err := os.Stat(rendered.document.filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file mode for %s at %s: %w", label, rendered.document.filePath, err)
|
||||
}
|
||||
originalMode = info.Mode().Perm()
|
||||
original = append([]byte(nil), rendered.document.raw...)
|
||||
}
|
||||
return &droidFileChange{
|
||||
filePath: rendered.document.filePath, label: label,
|
||||
original: original, existed: rendered.document.exists,
|
||||
next: append([]byte(nil), rendered.next...), mode: 0600, originalMode: originalMode,
|
||||
remove: rendered.remove,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func writeDroidChanges(changes []*droidFileChange, label string, rename droidRenameFunc) error {
|
||||
filtered := make([]droidFileChange, 0, len(changes))
|
||||
targets := map[string]bool{}
|
||||
for _, change := range changes {
|
||||
if change == nil || (change.existed && !change.remove && bytes.Equal(change.original, change.next)) {
|
||||
continue
|
||||
}
|
||||
target := filepath.Clean(change.filePath)
|
||||
if targets[target] {
|
||||
return fmt.Errorf("%s contains duplicate file target %s", label, change.filePath)
|
||||
}
|
||||
targets[target] = true
|
||||
filtered = append(filtered, *change)
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
if rename == nil {
|
||||
rename = os.Rename
|
||||
}
|
||||
staged := make([]droidStagedChange, 0, len(filtered))
|
||||
for _, change := range filtered {
|
||||
item, err := stageDroidChange(change)
|
||||
if err != nil {
|
||||
cleanupErrors := cleanupDroidStaging(staged)
|
||||
return joinDroidFailure(fmt.Errorf("%s: %w", label, err), cleanupErrors, "cleanup failed")
|
||||
}
|
||||
staged = append(staged, item)
|
||||
}
|
||||
for index := range staged {
|
||||
if err := commitDroidChange(&staged[index], rename); err != nil {
|
||||
recoveryErrors := rollbackDroidChanges(staged, rename)
|
||||
recoveryErrors = append(recoveryErrors, cleanupDroidStaging(staged)...)
|
||||
return joinDroidFailure(fmt.Errorf("%s: %w", label, err), recoveryErrors, "recovery failed")
|
||||
}
|
||||
}
|
||||
cleanupErrors := cleanupDroidStaging(staged)
|
||||
if len(cleanupErrors) > 0 {
|
||||
return joinDroidFailure(fmt.Errorf("%s cleanup failed", label), cleanupErrors, "cleanup failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stageDroidChange(change droidFileChange) (droidStagedChange, error) {
|
||||
if err := assertDroidUnchanged(change); err != nil {
|
||||
return droidStagedChange{}, err
|
||||
}
|
||||
directory := filepath.Dir(change.filePath)
|
||||
if err := os.MkdirAll(directory, 0700); err != nil {
|
||||
return droidStagedChange{}, fmt.Errorf("create directory for %s at %s: %w", change.label, directory, err)
|
||||
}
|
||||
staged := droidStagedChange{change: change}
|
||||
var err error
|
||||
if change.remove {
|
||||
staged.rollbackPath, err = reserveDroidPath(directory, filepath.Base(change.filePath), ".rollback")
|
||||
} else {
|
||||
staged.temporaryPath, err = writeDroidStagedFile(
|
||||
directory, filepath.Base(change.filePath), ".tmp", change.next, change.mode,
|
||||
)
|
||||
if err == nil && change.existed {
|
||||
staged.rollbackPath, err = writeDroidStagedFile(
|
||||
directory, filepath.Base(change.filePath), ".rollback", change.original, change.originalMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
cleanupErrors := cleanupDroidStaging([]droidStagedChange{staged})
|
||||
return droidStagedChange{}, joinDroidFailure(
|
||||
fmt.Errorf("stage %s: %w", change.label, err), cleanupErrors, "cleanup failed",
|
||||
)
|
||||
}
|
||||
return staged, nil
|
||||
}
|
||||
|
||||
func writeDroidStagedFile(
|
||||
directory, basename, suffix string,
|
||||
content []byte,
|
||||
mode os.FileMode,
|
||||
) (string, error) {
|
||||
file, err := os.CreateTemp(directory, "."+basename+".*"+suffix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := file.Name()
|
||||
failed := func(cause error) (string, error) {
|
||||
return "", errors.Join(cause, file.Close(), removeDroidOptional(path))
|
||||
}
|
||||
if err := file.Chmod(mode); err != nil {
|
||||
return failed(err)
|
||||
}
|
||||
written, err := file.Write(content)
|
||||
if err != nil {
|
||||
return failed(err)
|
||||
}
|
||||
if written != len(content) {
|
||||
return failed(io.ErrShortWrite)
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
return failed(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return "", errors.Join(err, removeDroidOptional(path))
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func reserveDroidPath(directory, basename, suffix string) (string, error) {
|
||||
file, err := os.CreateTemp(directory, "."+basename+".*"+suffix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := file.Name()
|
||||
if err := file.Close(); err != nil {
|
||||
return "", errors.Join(err, removeDroidOptional(path))
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func assertDroidUnchanged(change droidFileChange) error {
|
||||
current, exists, err := readDroidOptional(change.filePath, change.label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists != change.existed || !bytes.Equal(current, change.original) {
|
||||
return fmt.Errorf("%s changed during installation: %s", change.label, change.filePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func commitDroidChange(staged *droidStagedChange, rename droidRenameFunc) error {
|
||||
if err := assertDroidUnchanged(staged.change); err != nil {
|
||||
return err
|
||||
}
|
||||
if staged.change.remove {
|
||||
if staged.rollbackPath == "" {
|
||||
return fmt.Errorf("missing rollback path for %s", staged.change.label)
|
||||
}
|
||||
if err := rename(staged.change.filePath, staged.rollbackPath); err != nil {
|
||||
return fmt.Errorf("remove %s at %s: %w", staged.change.label, staged.change.filePath, err)
|
||||
}
|
||||
} else {
|
||||
if staged.temporaryPath == "" {
|
||||
return fmt.Errorf("missing staged file for %s", staged.change.label)
|
||||
}
|
||||
if err := rename(staged.temporaryPath, staged.change.filePath); err != nil {
|
||||
return fmt.Errorf("commit %s at %s: %w", staged.change.label, staged.change.filePath, err)
|
||||
}
|
||||
}
|
||||
staged.committed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func rollbackDroidChanges(staged []droidStagedChange, rename droidRenameFunc) []error {
|
||||
failures := make([]error, 0)
|
||||
for index := len(staged) - 1; index >= 0; index-- {
|
||||
item := &staged[index]
|
||||
if !item.committed {
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
switch {
|
||||
case item.change.remove:
|
||||
err = rename(item.rollbackPath, item.change.filePath)
|
||||
case item.change.existed:
|
||||
err = rename(item.rollbackPath, item.change.filePath)
|
||||
default:
|
||||
err = removeDroidOptional(item.change.filePath)
|
||||
}
|
||||
if err != nil {
|
||||
failures = append(failures, fmt.Errorf("restore %s at %s: %w", item.change.label, item.change.filePath, err))
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func cleanupDroidStaging(staged []droidStagedChange) []error {
|
||||
failures := make([]error, 0)
|
||||
for _, item := range staged {
|
||||
for _, path := range []string{item.temporaryPath, item.rollbackPath} {
|
||||
if err := removeDroidOptional(path); err != nil {
|
||||
failures = append(failures, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func removeDroidOptional(path string) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("remove staged Factory Droid file at %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func joinDroidFailure(cause error, related []error, label string) error {
|
||||
if len(related) == 0 {
|
||||
return cause
|
||||
}
|
||||
errorsWithContext := []error{cause, fmt.Errorf("%s: %w", label, errors.Join(related...))}
|
||||
return errors.Join(errorsWithContext...)
|
||||
}
|
||||
|
||||
func droidRuntimeFilesExist(contracts []droidRuntimeContract) (bool, error) {
|
||||
for _, contract := range contracts {
|
||||
configPath := filepath.Join(filepath.Dir(contract.guardPath), "config.json")
|
||||
raw, exists, err := readDroidOptional(configPath, "Elydora runtime config")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
var config map[string]any
|
||||
if err := json.Unmarshal(raw, &config); err != nil {
|
||||
return false, fmt.Errorf("parse Elydora runtime config at %s: %w", configPath, err)
|
||||
}
|
||||
agentID, ok := config["agent_id"].(string)
|
||||
if !ok || config["agent_name"] != droidAgentKey || !sameDroidAgentID(agentID, contract.agentID) {
|
||||
continue
|
||||
}
|
||||
guardExists, err := regularFileExists(contract.guardPath, "Elydora guard runtime")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
auditExists, err := regularFileExists(contract.auditPath, "Elydora audit runtime")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if guardExists && auditExists {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in new issue
Block a user