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,261 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func initializeDurableLayout(journal *durableJournal, changes []fileChange) error {
|
||||
directories := make(map[string]int)
|
||||
for index, change := range changes {
|
||||
directory := filepath.Dir(change.filePath)
|
||||
if err := ensureManagedDirectory(directory, "directory for "+change.label); err != nil {
|
||||
return err
|
||||
}
|
||||
key := durablePathKey(directory)
|
||||
workspaceIndex, exists := directories[key]
|
||||
if !exists {
|
||||
workspacePath := filepath.Join(directory, ".elydora-txn-"+journal.ID)
|
||||
workspaceIndex = len(journal.Workspaces)
|
||||
directories[key] = workspaceIndex
|
||||
journal.Workspaces = append(journal.Workspaces, durableWorkspace{
|
||||
Path: workspacePath, MarkerPath: filepath.Join(workspacePath, "owner"),
|
||||
OwnerToken: journal.OwnerToken,
|
||||
})
|
||||
}
|
||||
workspace := journal.Workspaces[workspaceIndex].Path
|
||||
entry := durableEntry{
|
||||
Path: change.filePath, Label: change.label, Workspace: workspace,
|
||||
DiscardPath: filepath.Join(workspace, fmt.Sprintf("%04d.discard", index)),
|
||||
Original: artifactFromChangeOriginal(change),
|
||||
}
|
||||
switch {
|
||||
case change.remove:
|
||||
entry.Kind = durableDelete
|
||||
entry.OriginalPath = filepath.Join(workspace, fmt.Sprintf("%04d.original", index))
|
||||
default:
|
||||
entry.NextPath = filepath.Join(workspace, fmt.Sprintf("%04d.next", index))
|
||||
entry.Next = durableArtifact{
|
||||
Exists: true, SHA256: artifactDigest(change.next), Mode: uint32(change.mode.Perm()),
|
||||
}
|
||||
if change.existed {
|
||||
entry.Kind = durableUpdate
|
||||
if atomicReplacementUsesSeparateBackup() {
|
||||
entry.OriginalPath = filepath.Join(
|
||||
workspace,
|
||||
fmt.Sprintf("%04d.original", index),
|
||||
)
|
||||
} else {
|
||||
entry.OriginalPath = entry.NextPath
|
||||
}
|
||||
} else {
|
||||
entry.Kind = durableCreate
|
||||
}
|
||||
}
|
||||
journal.Entries = append(journal.Entries, entry)
|
||||
}
|
||||
sort.Slice(journal.Workspaces, func(left, right int) bool {
|
||||
return durablePathKey(journal.Workspaces[left].Path) <
|
||||
durablePathKey(journal.Workspaces[right].Path)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func durablePathKey(path string) string {
|
||||
key := filepath.Clean(path)
|
||||
if runtime.GOOS == "windows" {
|
||||
return strings.ToLower(filepath.ToSlash(key))
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func createDurableWorkspaces(journal *durableJournal) error {
|
||||
for index := range journal.Workspaces {
|
||||
workspace := &journal.Workspaces[index]
|
||||
if err := createPrivateTransactionDirectory(workspace.Path); err != nil {
|
||||
return fmt.Errorf(
|
||||
"create transaction workspace %s: %w; recovery namespace %s",
|
||||
workspace.Path,
|
||||
err,
|
||||
journal.JournalDir,
|
||||
)
|
||||
}
|
||||
marker := []byte(journal.ID + ":" + workspace.OwnerToken + "\n")
|
||||
if err := writeOwnedFile(workspace.MarkerPath, marker, 0600); err != nil {
|
||||
return fmt.Errorf("write transaction workspace owner at %s: %w", workspace.Path, err)
|
||||
}
|
||||
if err := syncTransactionDirectory(workspace.Path); err != nil {
|
||||
return fmt.Errorf("persist transaction workspace owner at %s: %w", workspace.Path, err)
|
||||
}
|
||||
if err := syncTransactionDirectory(filepath.Dir(workspace.Path)); err != nil {
|
||||
return fmt.Errorf("persist transaction workspace at %s: %w", workspace.Path, err)
|
||||
}
|
||||
identity, err := transactionDirectoryIdentity(workspace.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workspace.DirectoryID = identity
|
||||
if err := appendDurableJournal(journal, durablePhaseInitializing); err != nil {
|
||||
return fmt.Errorf("persist transaction workspace identity at %s: %w", workspace.Path, err)
|
||||
}
|
||||
if err := probeAtomicTransactionCapabilities(workspace.Path); err != nil {
|
||||
return fmt.Errorf(
|
||||
"filesystem at %s lacks required durable transaction primitives: %w",
|
||||
workspace.Path,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func transactionDirectoryIdentity(path string) (string, error) {
|
||||
directory, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
info, statErr := directory.Stat()
|
||||
if statErr != nil || !info.IsDir() {
|
||||
return "", errors.Join(
|
||||
fmt.Errorf("transaction workspace is not a directory: %s", path),
|
||||
statErr,
|
||||
directory.Close(),
|
||||
)
|
||||
}
|
||||
identity, identityErr := transactionFileIdentity(directory, info)
|
||||
return identity, errors.Join(identityErr, directory.Close())
|
||||
}
|
||||
|
||||
func stageDurableEntries(journal *durableJournal, changes []fileChange) error {
|
||||
for index := range journal.Entries {
|
||||
entry := &journal.Entries[index]
|
||||
change := changes[index]
|
||||
if entry.Kind == durableDelete {
|
||||
continue
|
||||
}
|
||||
if err := writeOwnedFile(entry.NextPath, change.next, change.mode); err != nil {
|
||||
return fmt.Errorf("stage %s at %s: %w", change.label, entry.NextPath, err)
|
||||
}
|
||||
snapshot, err := readDurableArtifact(entry.NextPath, "staged "+change.label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry.Next = artifactFromSnapshot(snapshot)
|
||||
if entry.Next.SHA256 != artifactDigest(change.next) ||
|
||||
!sameManagedFileMode(snapshot.mode, change.mode) {
|
||||
return fmt.Errorf("staged %s changed before commit: %s", change.label, entry.NextPath)
|
||||
}
|
||||
if err := appendDurableJournal(journal, durablePhaseInitializing); err != nil {
|
||||
return fmt.Errorf("persist staged %s identity: %w", change.label, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func probeAtomicTransactionCapabilities(workspace string) (result error) {
|
||||
target := filepath.Join(workspace, "probe-target")
|
||||
replacement := filepath.Join(workspace, "probe-replacement")
|
||||
backup := replacement
|
||||
if atomicReplacementUsesSeparateBackup() {
|
||||
backup = filepath.Join(workspace, "probe-backup")
|
||||
}
|
||||
discard := filepath.Join(workspace, "probe-discard")
|
||||
left := filepath.Join(workspace, "probe-left")
|
||||
right := filepath.Join(workspace, "probe-right")
|
||||
defer func() {
|
||||
result = errors.Join(
|
||||
result,
|
||||
cleanupInterruptedCapabilityProbe(workspace),
|
||||
)
|
||||
}()
|
||||
for path, content := range map[string]string{target: "old", replacement: "new"} {
|
||||
if err := writeOwnedFile(path, []byte(content), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := atomicReplaceWithBackup(target, replacement, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
installed, err := readDurableArtifact(target, "atomic replacement probe")
|
||||
if err != nil || installed == nil || string(installed.contents) != "new" {
|
||||
return errors.Join(fmt.Errorf("atomic replacement probe failed"), err)
|
||||
}
|
||||
old, err := readDurableArtifact(backup, "atomic backup probe")
|
||||
if err != nil || old == nil || string(old.contents) != "old" {
|
||||
return errors.Join(fmt.Errorf("atomic replacement backup probe failed"), err)
|
||||
}
|
||||
if err := atomicReplaceWithBackup(target, backup, discardPathForProbe(backup, discard)); err != nil {
|
||||
return fmt.Errorf("atomic rollback probe failed: %w", err)
|
||||
}
|
||||
if err := writeOwnedFile(left, []byte("left"), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeOwnedFile(right, []byte("right"), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
leftBefore, err := readDurableArtifact(left, "atomic no-replace source probe")
|
||||
if err != nil || leftBefore == nil {
|
||||
return errors.Join(fmt.Errorf("read atomic no-replace source probe"), err)
|
||||
}
|
||||
rightBefore, err := readDurableArtifact(right, "atomic no-replace destination probe")
|
||||
if err != nil || rightBefore == nil {
|
||||
return errors.Join(fmt.Errorf("read atomic no-replace destination probe"), err)
|
||||
}
|
||||
renameErr := atomicRenameNoReplace(left, right)
|
||||
if renameErr == nil {
|
||||
return fmt.Errorf("atomic no-replace probe replaced an existing file")
|
||||
}
|
||||
if !isAtomicDestinationExists(renameErr) {
|
||||
return fmt.Errorf("atomic no-replace probe returned an unsupported error: %w", renameErr)
|
||||
}
|
||||
if _, err := requireDurableArtifact(
|
||||
left,
|
||||
"atomic no-replace source probe",
|
||||
artifactFromSnapshot(leftBefore),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := requireDurableArtifact(
|
||||
right,
|
||||
"atomic no-replace destination probe",
|
||||
artifactFromSnapshot(rightBefore),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discardPathForProbe(backup, discard string) string {
|
||||
if atomicReplacementUsesSeparateBackup() {
|
||||
return discard
|
||||
}
|
||||
return backup
|
||||
}
|
||||
|
||||
func cleanupInterruptedCapabilityProbe(workspace string) error {
|
||||
var failures []error
|
||||
for _, name := range []string{
|
||||
"probe-target",
|
||||
"probe-replacement",
|
||||
"probe-backup",
|
||||
"probe-discard",
|
||||
"probe-left",
|
||||
"probe-right",
|
||||
} {
|
||||
path := filepath.Join(workspace, name)
|
||||
snapshot, err := readManagedFile(path, "transaction capability probe", 64)
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
if snapshot != nil {
|
||||
failures = append(failures, removePhysicalOwnedFile(path, snapshot.identity))
|
||||
}
|
||||
}
|
||||
return errors.Join(errors.Join(failures...), syncTransactionDirectory(workspace))
|
||||
}
|
||||
Reference in new issue
Block a user