Files
Elydora-Go-SDK/cmd/elydora/plugins/transaction_cleanup.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

488 lines
16 KiB
Go

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
}