Files
Elydora-Go-SDK/cmd/elydora/plugins/transaction.go
T
ZacharyZhang-NY eb41b991bf feat(go): durable transaction engine and Kiro IDE 1.0 adapter
Split the monolithic transaction into focused modules: append-only
crash-recoverable journal under ~/.elydora/transactions with owner
tokens and per-transaction namespaces, staged atomic publication
(stage .pending, fsync, rename, fsync), platform-specific atomic
replacement (MoveFileEx, renameat2, renamex_np), private-directory
hardening per OS, lock-guarded recovery, and workspace cleanup that
removes only transaction-created empty directories.

Add the Kiro IDE 1.0 plugin: strict v1 hook schema with one enabled
PreToolUse guard and PostToolUse audit pair bound to the active
workspace, agent-to-workspace identity binding in runtime config, and
exact migration of the legacy elydora-audit.kiro.hook contract.

Gates: go test ./... and -race pass, go vet clean, govulncheck reports
zero reachable vulnerabilities, git diff --check clean.
2026-07-21 19:02:57 -04:00

256 lines
7.2 KiB
Go

package plugins
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
type renameFunc func(source, destination string) error
type replaceWithBackupFunc func(target, replacement, backup string) error
type fileChange struct {
filePath string
label string
original []byte
originalInfo os.FileInfo
originalID string
originalMode os.FileMode
existed bool
next []byte
mode os.FileMode
remove bool
}
type stagedChange struct {
change fileChange
temporaryPath string
temporaryInfo os.FileInfo
rollbackPath string
originalCaptured bool
committedInfo os.FileInfo
committed bool
}
type transactionFileOps struct {
moveNoReplace renameFunc
compensateNoReplace renameFunc
replaceWithBackup replaceWithBackupFunc
production bool
}
func newTransactionFileOps(rename renameFunc) transactionFileOps {
if rename == nil {
rename = atomicRenameNoReplace
return transactionFileOps{
moveNoReplace: rename,
compensateNoReplace: rename,
replaceWithBackup: atomicReplaceWithBackup,
production: true,
}
}
return transactionFileOps{
moveNoReplace: rename,
compensateNoReplace: atomicRenameNoReplace,
}
}
func sameManagedFileMode(current, expected os.FileMode) bool {
return runtime.GOOS == "windows" || current.Perm() == expected.Perm()
}
func readOptionalFile(path, label string) ([]byte, bool, error) {
snapshot, err := readManagedFile(path, label, maxManagedSourceBytes)
if err != nil {
return nil, false, err
}
if snapshot == nil {
return nil, false, nil
}
return append([]byte(nil), snapshot.contents...), true, nil
}
func prepareFileChange(filePath, label string, next []byte, mode os.FileMode) (*fileChange, error) {
original, existed, err := readOptionalFile(filePath, label)
if err != nil {
return nil, err
}
return prepareSourceChange(filePath, label, original, existed, next, mode, false)
}
func prepareSourceChange(
filePath, label string,
original []byte,
existed bool,
next []byte,
mode os.FileMode,
remove bool,
) (*fileChange, error) {
if !existed {
original = nil
}
if !remove && int64(len(next)) > maxManagedSourceBytes {
return nil, fmt.Errorf(
"%s exceeds %d bytes: %s",
label,
maxManagedSourceBytes,
filePath,
)
}
snapshot, err := readManagedFile(filePath, label, maxManagedSourceBytes)
if err != nil {
return nil, err
}
currentExists := snapshot != nil
if currentExists != existed ||
(currentExists && !bytes.Equal(snapshot.contents, original)) {
return nil, fmt.Errorf("%s changed before update: %s", label, filePath)
}
if existed && !remove && bytes.Equal(original, next) &&
sameManagedFileMode(snapshot.mode, mode) {
return nil, nil
}
if !existed && (remove || next == nil) {
return nil, nil
}
originalMode := mode
var originalInfo os.FileInfo
if existed {
originalInfo = snapshot.info
originalMode = snapshot.mode
}
return &fileChange{
filePath: filePath, label: label, original: append([]byte(nil), original...),
originalInfo: originalInfo, originalID: snapshotIdentity(snapshot),
originalMode: originalMode, existed: existed,
next: append([]byte(nil), next...),
mode: mode, remove: remove,
}, nil
}
func snapshotIdentity(snapshot *managedFileSnapshot) string {
if snapshot == nil {
return ""
}
return snapshot.identity
}
func writeChanges(
changes []*fileChange,
label string,
rename renameFunc,
preconditions ...filePrecondition,
) error {
return writeChangesWithFileOps(
changes,
label,
newTransactionFileOps(rename),
preconditions...,
)
}
func writeChangesWithFileOps(
changes []*fileChange,
label string,
ops transactionFileOps,
preconditions ...filePrecondition,
) error {
if ops.production {
return writeDurableChanges(changes, label, ops, preconditions...)
}
if ops.moveNoReplace == nil {
return fmt.Errorf("%s is missing atomic file operations", label)
}
filtered := make([]fileChange, 0, len(changes))
targets := map[string]bool{}
for _, change := range changes {
if change == nil || (change.existed && !change.remove &&
bytes.Equal(change.original, change.next) &&
sameManagedFileMode(change.originalMode, change.mode)) {
continue
}
target := filepath.Clean(change.filePath)
if runtime.GOOS == "windows" {
target = strings.ToLower(target)
}
if targets[target] {
return fmt.Errorf("%s contains duplicate file target %s", label, change.filePath)
}
targets[target] = true
filtered = append(filtered, *change)
}
if err := assertFilePreconditions(preconditions, label); err != nil {
return fmt.Errorf("%s: %w", label, err)
}
if len(filtered) == 0 {
return nil
}
staged := make([]stagedChange, 0, len(filtered))
for _, change := range filtered {
if err := assertFilePreconditions(preconditions, label); err != nil {
cleanupErrors := cleanupStaging(staged)
return joinTransactionFailure(
fmt.Errorf("%s: %w", label, err),
cleanupErrors,
"cleanup failed",
)
}
item, err := stageChange(change)
if err != nil {
cleanupErrors := cleanupStaging(staged)
return joinTransactionFailure(fmt.Errorf("%s: %w", label, err), cleanupErrors, "cleanup failed")
}
staged = append(staged, item)
if err := assertFilePreconditions(preconditions, label); err != nil {
cleanupErrors := cleanupStaging(staged)
return joinTransactionFailure(
fmt.Errorf("%s: %w", label, err),
cleanupErrors,
"cleanup failed",
)
}
}
if err := assertFilePreconditions(preconditions, label); err != nil {
cleanupErrors := cleanupStaging(staged)
return joinTransactionFailure(fmt.Errorf("%s: %w", label, err), cleanupErrors, "cleanup failed")
}
for index := range staged {
if err := assertFilePreconditions(preconditions, label); err != nil {
recoveryErrors := rollbackChanges(staged, ops)
recoveryErrors = append(recoveryErrors, cleanupStaging(staged)...)
return joinTransactionFailure(fmt.Errorf("%s: %w", label, err), recoveryErrors, "recovery failed")
}
if err := commitChange(&staged[index], ops); err != nil {
recoveryErrors := rollbackChanges(staged, ops)
recoveryErrors = append(recoveryErrors, cleanupStaging(staged)...)
return joinTransactionFailure(fmt.Errorf("%s: %w", label, err), recoveryErrors, "recovery failed")
}
}
if err := assertFilePreconditions(preconditions, label); err != nil {
recoveryErrors := rollbackChanges(staged, ops)
recoveryErrors = append(recoveryErrors, cleanupStaging(staged)...)
return joinTransactionFailure(fmt.Errorf("%s: %w", label, err), recoveryErrors, "recovery failed")
}
if err := assertCommittedChanges(staged); err != nil {
recoveryErrors := rollbackChanges(staged, ops)
recoveryErrors = append(recoveryErrors, cleanupStaging(staged)...)
return joinTransactionFailure(fmt.Errorf("%s: %w", label, err), recoveryErrors, "recovery failed")
}
cleanupErrors := cleanupStaging(staged)
if len(cleanupErrors) > 0 {
return joinTransactionFailure(fmt.Errorf("%s cleanup failed", label), cleanupErrors, "cleanup failed")
}
return nil
}
func joinTransactionFailure(cause error, related []error, label string) error {
if len(related) == 0 {
return cause
}
return errors.Join(cause, fmt.Errorf("%s: %w", label, errors.Join(related...)))
}