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.
This commit is contained in:
1 parent
dd72793cdb
commit
eb41b991bf
70 files changed
+8182
-358
No files matched your search
@@ -0,0 +1,236 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func writeDurableChanges(
|
||||
changes []*fileChange,
|
||||
label string,
|
||||
ops transactionFileOps,
|
||||
preconditions ...filePrecondition,
|
||||
) (result error) {
|
||||
if err := transactionPlatformSupported(); err != nil {
|
||||
return fmt.Errorf("%s: %w", label, err)
|
||||
}
|
||||
if ops.moveNoReplace == nil || ops.replaceWithBackup == nil {
|
||||
return fmt.Errorf("%s is missing durable atomic file operations", label)
|
||||
}
|
||||
stateRoot, err := durableTransactionStateRoot()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: prepare durable transaction state: %w", label, err)
|
||||
}
|
||||
lock, err := acquireDurableTransactionLock(stateRoot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", label, err)
|
||||
}
|
||||
defer func() {
|
||||
result = errors.Join(result, lock.close())
|
||||
}()
|
||||
if err := recoverPendingDurableTransactions(stateRoot, ops); err != nil {
|
||||
return fmt.Errorf("%s: pending transaction recovery failed: %w", label, err)
|
||||
}
|
||||
filtered, err := filterDurableChanges(changes, label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assertFilePreconditions(preconditions, label); err != nil {
|
||||
return fmt.Errorf("%s: %w", label, err)
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
journal, err := createDurableJournal(stateRoot, label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initializeDurableLayout(journal, filtered); err != nil {
|
||||
return preserveUnjournaledNamespace(journal, err)
|
||||
}
|
||||
if err := appendDurableJournal(journal, durablePhaseInitializing); err != nil {
|
||||
return preserveUnjournaledNamespace(journal, err)
|
||||
}
|
||||
abort := func(cause error) error {
|
||||
return abortDurableTransaction(journal, ops, cause)
|
||||
}
|
||||
if err := createDurableWorkspaces(journal); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
if err := stageDurableEntries(journal, filtered); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
if err := appendDurableJournal(journal, durablePhasePrepared); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
if err := assertDurableOriginals(journal); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
if err := assertFilePreconditions(preconditions, label); err != nil {
|
||||
return abort(fmt.Errorf("%s: %w", label, err))
|
||||
}
|
||||
for index := range journal.Entries {
|
||||
if err := assertFilePreconditions(preconditions, label); err != nil {
|
||||
return abort(fmt.Errorf("%s: %w", label, err))
|
||||
}
|
||||
activeIndex := index
|
||||
journal.ActiveEntry = &activeIndex
|
||||
if err := appendDurableJournal(journal, durablePhaseCommitting); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
if err := commitDurableEntry(&journal.Entries[index], ops); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
journal.CompletedEntries = index + 1
|
||||
journal.ActiveEntry = nil
|
||||
if err := appendDurableJournal(journal, durablePhaseCommitting); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
}
|
||||
if err := assertFilePreconditions(preconditions, label); err != nil {
|
||||
return abort(fmt.Errorf("%s: %w", label, err))
|
||||
}
|
||||
if err := verifyDurableJournalCommitted(journal); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
if err := appendDurableJournal(journal, durablePhaseCommitted); err != nil {
|
||||
return abort(err)
|
||||
}
|
||||
if err := finishCommittedDurableTransaction(journal); err != nil {
|
||||
return fmt.Errorf(
|
||||
"%s committed; durable cleanup failed: %w; recovery journal %s",
|
||||
label,
|
||||
err,
|
||||
journal.JournalDir,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterDurableChanges(changes []*fileChange, label string) ([]fileChange, error) {
|
||||
filtered := make([]fileChange, 0, len(changes))
|
||||
targets := make(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, err := filepath.Abs(change.filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"%s cannot resolve target %s: %w",
|
||||
label,
|
||||
change.filePath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
target = filepath.Clean(target)
|
||||
change.filePath = target
|
||||
if runtime.GOOS == "windows" {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
if targets[target] {
|
||||
return nil, fmt.Errorf("%s contains duplicate file target %s", label, change.filePath)
|
||||
}
|
||||
targets[target] = true
|
||||
filtered = append(filtered, *change)
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func preserveUnjournaledNamespace(journal *durableJournal, cause error) error {
|
||||
return fmt.Errorf(
|
||||
"%w; transaction namespace preserved at %s",
|
||||
cause,
|
||||
journal.JournalDir,
|
||||
)
|
||||
}
|
||||
|
||||
func assertDurableOriginals(journal *durableJournal) error {
|
||||
for _, entry := range journal.Entries {
|
||||
if _, err := requireDurableArtifact(entry.Path, entry.Label, entry.Original); err != nil {
|
||||
return fmt.Errorf("%s changed during installation: %w", entry.Label, err)
|
||||
}
|
||||
if entry.NextPath != "" {
|
||||
if _, err := requireDurableArtifact(
|
||||
entry.NextPath,
|
||||
"staged "+entry.Label,
|
||||
entry.Next,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func commitDurableEntry(entry *durableEntry, ops transactionFileOps) error {
|
||||
if _, err := requireDurableArtifact(entry.Path, entry.Label, entry.Original); err != nil {
|
||||
return fmt.Errorf("%s changed at commit boundary: %w", entry.Label, err)
|
||||
}
|
||||
switch entry.Kind {
|
||||
case durableCreate:
|
||||
if err := ops.moveNoReplace(entry.NextPath, entry.Path); err != nil {
|
||||
return fmt.Errorf("create %s without replacement: %w", entry.Label, err)
|
||||
}
|
||||
case durableUpdate:
|
||||
if _, err := requireDurableArtifact(
|
||||
entry.NextPath,
|
||||
"staged "+entry.Label,
|
||||
entry.Next,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.OriginalPath != entry.NextPath {
|
||||
if current, err := readDurableArtifact(entry.OriginalPath, "backup "+entry.Label); err != nil || current != nil {
|
||||
return errors.Join(fmt.Errorf("backup path is occupied: %s", entry.OriginalPath), err)
|
||||
}
|
||||
}
|
||||
if err := ops.replaceWithBackup(entry.Path, entry.NextPath, entry.OriginalPath); err != nil {
|
||||
return fmt.Errorf("atomically replace %s: %w", entry.Label, err)
|
||||
}
|
||||
case durableDelete:
|
||||
if err := ops.moveNoReplace(entry.Path, entry.OriginalPath); err != nil {
|
||||
return fmt.Errorf("capture deleted %s: %w", entry.Label, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown durable change kind %q", entry.Kind)
|
||||
}
|
||||
if err := verifyDurableEntryCommitted(entry); err != nil {
|
||||
return fmt.Errorf("%s changed immediately after durable commit: %w", entry.Label, err)
|
||||
}
|
||||
return syncTransactionDirectory(filepath.Dir(entry.Path))
|
||||
}
|
||||
|
||||
func verifyDurableEntryCommitted(entry *durableEntry) error {
|
||||
wantTarget := entry.Next
|
||||
if entry.Kind == durableDelete {
|
||||
wantTarget = durableArtifact{}
|
||||
}
|
||||
if _, err := requireDurableArtifact(entry.Path, entry.Label, wantTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
switch entry.Kind {
|
||||
case durableUpdate, durableDelete:
|
||||
_, err := requireDurableArtifact(entry.OriginalPath, "original "+entry.Label, entry.Original)
|
||||
return err
|
||||
case durableCreate:
|
||||
_, err := requireDurableArtifact(entry.NextPath, "consumed staged "+entry.Label, durableArtifact{})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyDurableJournalCommitted(journal *durableJournal) error {
|
||||
for index := range journal.Entries {
|
||||
if err := verifyDurableEntryCommitted(&journal.Entries[index]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user