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,487 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type durableCleanupDisposition string
|
||||
|
||||
const (
|
||||
cleanupCommitted durableCleanupDisposition = "committed"
|
||||
cleanupRolledBack durableCleanupDisposition = "rolled_back"
|
||||
)
|
||||
|
||||
func finishCommittedDurableTransaction(journal *durableJournal) error {
|
||||
if err := verifyCommittedDurableTransactionForCleanup(journal); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, workspace := range journal.Workspaces {
|
||||
if err := cleanupDurableWorkspace(journal, workspace, cleanupCommitted); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return cleanupDurableJournalNamespace(journal)
|
||||
}
|
||||
|
||||
func verifyCommittedDurableTransactionForCleanup(journal *durableJournal) error {
|
||||
for index := range journal.Entries {
|
||||
entry := &journal.Entries[index]
|
||||
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:
|
||||
original, err := readDurableArtifact(entry.OriginalPath, "original "+entry.Label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if original != nil && !ownedArtifactMatches(entry.Original, original) {
|
||||
return fmt.Errorf("committed original changed; preserved at %s", entry.OriginalPath)
|
||||
}
|
||||
case durableCreate:
|
||||
if _, err := requireDurableArtifact(
|
||||
entry.NextPath,
|
||||
"consumed staged "+entry.Label,
|
||||
durableArtifact{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown durable change kind %q", entry.Kind)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupRolledBackDurableTransaction(journal *durableJournal) error {
|
||||
for index := range journal.Entries {
|
||||
entry := &journal.Entries[index]
|
||||
if durableEntryDefinitelyUnattempted(journal, index) {
|
||||
continue
|
||||
}
|
||||
if journal.ActiveEntry != nil && *journal.ActiveEntry == index {
|
||||
uncommitted, err := durableEntryHasUncommittedWorkspaceState(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if uncommitted {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, err := requireDurableArtifact(entry.Path, entry.Label, entry.Original); err != nil {
|
||||
return fmt.Errorf("rollback verification failed for %s: %w", entry.Label, err)
|
||||
}
|
||||
}
|
||||
for _, workspace := range journal.Workspaces {
|
||||
if err := cleanupDurableWorkspace(journal, workspace, cleanupRolledBack); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return cleanupDurableJournalNamespace(journal)
|
||||
}
|
||||
|
||||
func durableEntryHasUncommittedWorkspaceState(entry *durableEntry) (bool, error) {
|
||||
switch entry.Kind {
|
||||
case durableCreate, durableUpdate:
|
||||
return durableEntryHasStagedNext(entry)
|
||||
case durableDelete:
|
||||
original, err := readDurableArtifact(entry.OriginalPath, "captured deleted "+entry.Label)
|
||||
return original == nil, err
|
||||
default:
|
||||
return false, fmt.Errorf("unknown durable change kind %q", entry.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupDurableWorkspace(
|
||||
journal *durableJournal,
|
||||
workspace durableWorkspace,
|
||||
disposition durableCleanupDisposition,
|
||||
) error {
|
||||
info, err := os.Lstat(workspace.Path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return errors.Join(fmt.Errorf("transaction workspace changed: %s", workspace.Path), err)
|
||||
}
|
||||
if err := verifyPrivateTransactionDirectory(workspace.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
if workspace.DirectoryID == "" {
|
||||
return fmt.Errorf("transaction workspace identity is unavailable; preserved at %s", workspace.Path)
|
||||
}
|
||||
identity, identityErr := transactionDirectoryIdentity(workspace.Path)
|
||||
if identityErr != nil || identity != workspace.DirectoryID {
|
||||
return errors.Join(
|
||||
fmt.Errorf("transaction workspace identity changed: %s", workspace.Path),
|
||||
identityErr,
|
||||
)
|
||||
}
|
||||
marker, err := readManagedFile(
|
||||
workspace.MarkerPath,
|
||||
"transaction workspace owner",
|
||||
maxDurableJournalBytes,
|
||||
)
|
||||
wantMarker := journal.ID + ":" + workspace.OwnerToken + "\n"
|
||||
if err != nil {
|
||||
return errors.Join(fmt.Errorf("transaction workspace owner changed: %s", workspace.Path), err)
|
||||
}
|
||||
if marker == nil {
|
||||
entries, readErr := os.ReadDir(workspace.Path)
|
||||
if readErr != nil || len(entries) != 0 {
|
||||
return errors.Join(
|
||||
fmt.Errorf("transaction workspace owner changed: %s", workspace.Path),
|
||||
readErr,
|
||||
)
|
||||
}
|
||||
identity, identityErr = transactionDirectoryIdentity(workspace.Path)
|
||||
if identityErr != nil || identity != workspace.DirectoryID {
|
||||
return errors.Join(
|
||||
fmt.Errorf("transaction workspace changed before resumed removal: %s", workspace.Path),
|
||||
identityErr,
|
||||
)
|
||||
}
|
||||
if err := os.Remove(workspace.Path); err != nil {
|
||||
return fmt.Errorf("remove empty transaction workspace %s: %w", workspace.Path, err)
|
||||
}
|
||||
return syncTransactionDirectory(filepath.Dir(workspace.Path))
|
||||
}
|
||||
if string(marker.contents) != wantMarker {
|
||||
return fmt.Errorf("transaction workspace owner changed: %s", workspace.Path)
|
||||
}
|
||||
if err := cleanupInterruptedCapabilityProbe(workspace.Path); err != nil {
|
||||
return fmt.Errorf("clean interrupted transaction capability probe: %w", err)
|
||||
}
|
||||
expected := expectedWorkspaceArtifacts(journal, workspace.Path, disposition)
|
||||
entries, err := os.ReadDir(workspace.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range entries {
|
||||
path := filepath.Join(workspace.Path, item.Name())
|
||||
if path == workspace.MarkerPath {
|
||||
continue
|
||||
}
|
||||
allowed, known := expected[path]
|
||||
if !known {
|
||||
return fmt.Errorf("unexpected object preserved in transaction workspace: %s", path)
|
||||
}
|
||||
if err := removeExpectedOwnedArtifact(path, allowed); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
identity, identityErr = transactionDirectoryIdentity(workspace.Path)
|
||||
if identityErr != nil || identity != workspace.DirectoryID {
|
||||
return errors.Join(
|
||||
fmt.Errorf("transaction workspace changed before removal: %s", workspace.Path),
|
||||
identityErr,
|
||||
)
|
||||
}
|
||||
if err := syncTransactionDirectory(workspace.Path); err != nil {
|
||||
return fmt.Errorf("persist transaction workspace artifact cleanup at %s: %w", workspace.Path, err)
|
||||
}
|
||||
if err := removePhysicalOwnedFile(workspace.MarkerPath, marker.identity); err != nil {
|
||||
return fmt.Errorf("remove transaction workspace owner at %s: %w", workspace.MarkerPath, err)
|
||||
}
|
||||
if err := syncTransactionDirectory(workspace.Path); err != nil {
|
||||
return fmt.Errorf("persist transaction workspace owner cleanup at %s: %w", workspace.Path, err)
|
||||
}
|
||||
if err := os.Remove(workspace.Path); err != nil {
|
||||
return fmt.Errorf("remove empty transaction workspace %s: %w", workspace.Path, err)
|
||||
}
|
||||
return syncTransactionDirectory(filepath.Dir(workspace.Path))
|
||||
}
|
||||
|
||||
func expectedWorkspaceArtifacts(
|
||||
journal *durableJournal,
|
||||
workspace string,
|
||||
disposition durableCleanupDisposition,
|
||||
) map[string][]durableArtifact {
|
||||
expected := make(map[string][]durableArtifact)
|
||||
add := func(path string, artifact durableArtifact) {
|
||||
if path != "" && filepath.Clean(filepath.Dir(path)) == filepath.Clean(workspace) {
|
||||
expected[path] = append(expected[path], artifact)
|
||||
}
|
||||
}
|
||||
for _, entry := range journal.Entries {
|
||||
if filepath.Clean(entry.Workspace) != filepath.Clean(workspace) {
|
||||
continue
|
||||
}
|
||||
if disposition == cleanupCommitted {
|
||||
switch entry.Kind {
|
||||
case durableUpdate, durableDelete:
|
||||
add(entry.OriginalPath, entry.Original)
|
||||
}
|
||||
} else {
|
||||
add(entry.NextPath, entry.Next)
|
||||
add(entry.OriginalPath, entry.Next)
|
||||
add(entry.OriginalPath, entry.Original)
|
||||
add(entry.DiscardPath, entry.Next)
|
||||
}
|
||||
add(entry.NextPath, durableArtifact{})
|
||||
add(entry.OriginalPath, durableArtifact{})
|
||||
add(entry.DiscardPath, durableArtifact{})
|
||||
}
|
||||
captures := make(map[string][]durableArtifact, len(expected))
|
||||
for path, artifacts := range expected {
|
||||
captures[path+".cleanup"] = artifacts
|
||||
}
|
||||
for path, artifacts := range captures {
|
||||
expected[path] = artifacts
|
||||
}
|
||||
return expected
|
||||
}
|
||||
|
||||
func removeExpectedOwnedArtifact(path string, allowed []durableArtifact) error {
|
||||
current, err := readDurableArtifact(path, "transaction artifact")
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect owned transaction artifact at %s: %w", path, err)
|
||||
}
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
for _, expected := range allowed {
|
||||
if ownedArtifactMatches(expected, current) {
|
||||
if strings.HasSuffix(path, ".cleanup") {
|
||||
return removePhysicalOwnedFile(path, current.identity)
|
||||
}
|
||||
return captureAndRemoveExpectedArtifact(path, allowed)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("changed transaction artifact preserved at %s", path)
|
||||
}
|
||||
|
||||
func captureAndRemoveExpectedArtifact(path string, allowed []durableArtifact) error {
|
||||
capture := path + ".cleanup"
|
||||
occupied, err := readDurableArtifact(capture, "cleanup capture")
|
||||
if err != nil || occupied != nil {
|
||||
return errors.Join(fmt.Errorf("cleanup capture path is occupied: %s", capture), err)
|
||||
}
|
||||
if err := atomicRenameNoReplace(path, capture); err != nil {
|
||||
return fmt.Errorf("atomically capture owned transaction artifact at %s: %w", path, err)
|
||||
}
|
||||
captured, err := readDurableArtifact(capture, "captured transaction artifact")
|
||||
if err == nil {
|
||||
for _, expected := range allowed {
|
||||
if ownedArtifactMatches(expected, captured) {
|
||||
return removePhysicalOwnedFile(capture, captured.identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
restoreErr := atomicRenameNoReplace(capture, path)
|
||||
return errors.Join(
|
||||
fmt.Errorf("changed transaction artifact preserved at %s", capture),
|
||||
err,
|
||||
restoreErr,
|
||||
)
|
||||
}
|
||||
|
||||
func ownedArtifactMatches(expected durableArtifact, current *managedFileSnapshot) bool {
|
||||
if current == nil || !expected.Exists || expected.Identity == "" {
|
||||
return false
|
||||
}
|
||||
if current.identity != expected.Identity {
|
||||
return false
|
||||
}
|
||||
return artifactDigest(current.contents) == expected.SHA256 &&
|
||||
sameManagedFileMode(current.mode, os.FileMode(expected.Mode))
|
||||
}
|
||||
|
||||
func removePhysicalOwnedFile(path string, expectedIdentity ...string) error {
|
||||
current, err := readManagedFile(path, "owned transaction artifact", maxDurableJournalBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
if len(expectedIdentity) > 0 && expectedIdentity[0] != "" &&
|
||||
current.identity != expectedIdentity[0] {
|
||||
return fmt.Errorf("owned transaction object changed identity; preserved at %s", path)
|
||||
}
|
||||
latest, err := os.Lstat(path)
|
||||
if err != nil || latest.Mode()&os.ModeSymlink != 0 ||
|
||||
!latest.Mode().IsRegular() || !os.SameFile(current.info, latest) {
|
||||
return errors.Join(
|
||||
fmt.Errorf("owned transaction object changed before removal; preserved at %s", path),
|
||||
err,
|
||||
)
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
return fmt.Errorf("remove owned transaction artifact at %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupDurableJournalNamespace(journal *durableJournal) error {
|
||||
return cleanupOwnedJournalDirectory(
|
||||
journal.JournalDir,
|
||||
journal.ID,
|
||||
journal.OwnerToken,
|
||||
journal.Sequence,
|
||||
)
|
||||
}
|
||||
|
||||
func cleanupEmptyDurableJournal(directory string) error {
|
||||
base := filepath.Base(directory)
|
||||
if !strings.HasPrefix(base, "txn-") {
|
||||
return fmt.Errorf("invalid empty journal namespace: %s", directory)
|
||||
}
|
||||
if err := verifyPrivateTransactionDirectory(directory); err != nil {
|
||||
return err
|
||||
}
|
||||
entries, readErr := os.ReadDir(directory)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
if err := os.Remove(directory); err != nil {
|
||||
return fmt.Errorf("remove empty recovery namespace %s: %w", directory, err)
|
||||
}
|
||||
return syncTransactionDirectory(filepath.Dir(directory))
|
||||
}
|
||||
type incompleteJournalFile struct {
|
||||
path string
|
||||
identity string
|
||||
}
|
||||
files := make([]incompleteJournalFile, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || entry.Name() != "owner" && !isDurableJournalPending(entry.Name()) {
|
||||
return fmt.Errorf("invalid journal state preserved for manual recovery at %s", directory)
|
||||
}
|
||||
path := filepath.Join(directory, entry.Name())
|
||||
snapshot, err := readManagedFile(path, "incomplete transaction journal file", maxDurableJournalBytes)
|
||||
if err != nil || snapshot == nil {
|
||||
return errors.Join(fmt.Errorf("incomplete journal file changed: %s", path), err)
|
||||
}
|
||||
files = append(files, incompleteJournalFile{path: path, identity: snapshot.identity})
|
||||
}
|
||||
for _, file := range files {
|
||||
if err := removePhysicalOwnedFile(file.path, file.identity); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := syncTransactionDirectory(directory); err != nil {
|
||||
return fmt.Errorf("persist incomplete journal file cleanup at %s: %w", directory, err)
|
||||
}
|
||||
if err := os.Remove(directory); err != nil {
|
||||
return fmt.Errorf("remove incomplete recovery namespace %s: %w", directory, err)
|
||||
}
|
||||
return syncTransactionDirectory(filepath.Dir(directory))
|
||||
}
|
||||
|
||||
func isDurableJournalPending(name string) bool {
|
||||
_, pending := durableJournalPendingSequence(name)
|
||||
return pending
|
||||
}
|
||||
|
||||
func cleanupOwnedJournalDirectory(
|
||||
directory, id, ownerToken string,
|
||||
latestSequence uint64,
|
||||
) error {
|
||||
wantOwner := id + ":" + ownerToken + "\n"
|
||||
ownerPath := filepath.Join(directory, "owner")
|
||||
owner, err := readManagedFile(ownerPath, "transaction journal owner", maxDurableJournalBytes)
|
||||
if err != nil || owner == nil || string(owner.contents) != wantOwner {
|
||||
return errors.Join(fmt.Errorf("journal owner changed; preserved at %s", directory), err)
|
||||
}
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type journalFileRemoval struct {
|
||||
path string
|
||||
identity string
|
||||
}
|
||||
recoverableFiles := make([]journalFileRemoval, 0)
|
||||
validStates := make([]journalFileRemoval, 0)
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == "owner" {
|
||||
continue
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return fmt.Errorf("unexpected journal object preserved at %s", filepath.Join(directory, entry.Name()))
|
||||
}
|
||||
path := filepath.Join(directory, entry.Name())
|
||||
if _, pending := durableJournalPendingSequence(entry.Name()); pending {
|
||||
snapshot, err := inspectRecoverableJournalFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recoverableFiles = append(
|
||||
recoverableFiles,
|
||||
journalFileRemoval{path: path, identity: snapshot.identity},
|
||||
)
|
||||
continue
|
||||
}
|
||||
sequence, valid := durableJournalStateSequence(entry.Name())
|
||||
if !valid {
|
||||
return fmt.Errorf("unexpected journal file preserved at %s", filepath.Join(directory, entry.Name()))
|
||||
}
|
||||
state, stateErr := readDurableJournalState(path)
|
||||
if stateErr != nil {
|
||||
if sequence > latestSequence {
|
||||
snapshot, err := inspectRecoverableJournalFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recoverableFiles = append(
|
||||
recoverableFiles,
|
||||
journalFileRemoval{path: path, identity: snapshot.identity},
|
||||
)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("invalid journal file preserved at %s: %w", path, stateErr)
|
||||
}
|
||||
if err := validateLoadedJournal(state, directory, entry.Name()); err != nil {
|
||||
return fmt.Errorf("changed journal file preserved at %s: %w", path, err)
|
||||
}
|
||||
snapshot, err := readManagedFile(path, "transaction journal state", maxDurableJournalBytes)
|
||||
if err != nil || snapshot == nil {
|
||||
return errors.Join(fmt.Errorf("transaction journal state changed: %s", path), err)
|
||||
}
|
||||
validStates = append(
|
||||
validStates,
|
||||
journalFileRemoval{path: path, identity: snapshot.identity},
|
||||
)
|
||||
}
|
||||
for _, file := range recoverableFiles {
|
||||
if err := removePhysicalOwnedFile(file.path, file.identity); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, file := range validStates {
|
||||
if err := removePhysicalOwnedFile(file.path, file.identity); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := syncTransactionDirectory(directory); err != nil {
|
||||
return fmt.Errorf("persist transaction journal state cleanup at %s: %w", directory, err)
|
||||
}
|
||||
if err := removePhysicalOwnedFile(ownerPath, owner.identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncTransactionDirectory(directory); err != nil {
|
||||
return fmt.Errorf("persist transaction journal owner cleanup at %s: %w", directory, err)
|
||||
}
|
||||
if err := os.Remove(directory); err != nil {
|
||||
return err
|
||||
}
|
||||
return syncTransactionDirectory(filepath.Dir(directory))
|
||||
}
|
||||
|
||||
func inspectRecoverableJournalFile(path string) (*managedFileSnapshot, error) {
|
||||
snapshot, err := readManagedFile(path, "recoverable transaction journal state", maxDurableJournalBytes)
|
||||
if err != nil || snapshot == nil {
|
||||
return nil, errors.Join(fmt.Errorf("transaction journal state changed: %s", path), err)
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
Reference in new issue
Block a user