Files
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

463 lines
11 KiB
Go

package plugins
import (
"errors"
"fmt"
"path/filepath"
)
func commitChange(item *stagedChange, ops transactionFileOps) error {
if err := assertFileUnchanged(item.change); err != nil {
return err
}
if item.change.existed {
if err := captureOriginalFile(item, ops); err != nil {
return err
}
}
if item.change.remove {
item.committed = true
return nil
}
if item.temporaryPath == "" || item.temporaryInfo == nil {
return fmt.Errorf("missing staged file for %s", item.change.label)
}
if err := ops.moveNoReplace(item.temporaryPath, item.change.filePath); err != nil {
failure := fmt.Errorf(
"commit %s at %s without replacing another file: %w",
item.change.label,
item.change.filePath,
err,
)
return errors.Join(failure, compensateFailedCommit(item, ops))
}
item.temporaryPath = ""
item.committedInfo = item.temporaryInfo
item.committed = true
if err := assertCommittedFileUnchanged(item); err != nil {
return fmt.Errorf("%w immediately after commit", err)
}
return nil
}
func compensateFailedCommit(item *stagedChange, ops transactionFileOps) error {
if !item.originalCaptured {
return nil
}
restoreErr := ops.moveNoReplace(item.rollbackPath, item.change.filePath)
if restoreErr == nil {
return finishOriginalRestore(item, ops)
}
current, inspectErr := readManagedFile(
item.change.filePath,
item.change.label,
maxManagedSourceBytes,
)
if inspectErr == nil && snapshotMatches(
current,
true,
item.change.original,
item.change.originalInfo,
item.change.originalMode,
) {
item.rollbackPath = ""
item.originalCaptured = false
return nil
}
var fallbackErr error
if inspectErr == nil && current == nil {
compensate := ops.compensateNoReplace
if compensate == nil {
compensate = atomicRenameNoReplace
}
fallbackErr = compensate(item.rollbackPath, item.change.filePath)
if fallbackErr == nil {
return finishOriginalRestore(item, ops)
}
}
cause := errors.Join(
fmt.Errorf("restore %s after failed commit at %s", item.change.label, item.change.filePath),
restoreErr,
fallbackErr,
inspectErr,
)
return preserveRollbackFile(item, cause)
}
func captureOriginalFile(item *stagedChange, ops transactionFileOps) error {
if item.rollbackPath == "" {
return fmt.Errorf("missing rollback path for %s", item.change.label)
}
if err := ops.moveNoReplace(item.change.filePath, item.rollbackPath); err != nil {
return fmt.Errorf(
"capture original %s at %s: %w",
item.change.label,
item.change.filePath,
err,
)
}
current, err := readManagedFile(
item.rollbackPath,
"captured "+item.change.label,
maxManagedSourceBytes,
)
if err != nil || !snapshotMatches(
current,
true,
item.change.original,
item.change.originalInfo,
item.change.originalMode,
) {
cause := err
if cause == nil {
cause = fmt.Errorf(
"%s changed at the atomic capture boundary: %s",
item.change.label,
item.change.filePath,
)
}
return restoreUnexpectedCapture(item, current, cause, ops)
}
item.originalCaptured = true
return nil
}
func restoreUnexpectedCapture(
item *stagedChange,
captured *managedFileSnapshot,
cause error,
ops transactionFileOps,
) error {
path := item.rollbackPath
if err := ops.moveNoReplace(path, item.change.filePath); err != nil {
item.rollbackPath = ""
return errors.Join(
cause,
fmt.Errorf(
"restore concurrently captured %s at %s: %w; captured object preserved at %s",
item.change.label,
item.change.filePath,
err,
path,
),
)
}
if captured != nil {
restored, err := readManagedFile(
item.change.filePath,
"restored concurrent "+item.change.label,
maxManagedSourceBytes,
)
if err != nil || !snapshotMatches(
restored,
true,
captured.contents,
captured.info,
captured.mode,
) {
if preserveErr := ops.moveNoReplace(item.change.filePath, path); preserveErr != nil {
item.rollbackPath = ""
return errors.Join(
cause,
err,
fmt.Errorf(
"concurrent %s changed while restoring: %w; objects left at %s and %s",
item.change.label,
preserveErr,
item.change.filePath,
path,
),
)
}
item.rollbackPath = ""
return errors.Join(
cause,
err,
fmt.Errorf("changed concurrent object preserved at %s", path),
)
}
}
item.rollbackPath = ""
return cause
}
func assertCommittedChanges(staged []stagedChange) error {
for index := range staged {
item := &staged[index]
if item.committed {
if err := assertCommittedFileUnchanged(item); err != nil {
return err
}
}
}
return nil
}
func assertCommittedFileUnchanged(item *stagedChange) error {
current, err := readManagedFile(
item.change.filePath,
item.change.label,
maxManagedSourceBytes,
)
if err != nil {
return err
}
if item.change.remove {
if current == nil {
return nil
}
} else if snapshotMatches(
current,
true,
item.change.next,
item.committedInfo,
item.change.mode,
) {
return nil
}
return fmt.Errorf(
"%s changed during transaction recovery: %s",
item.change.label,
item.change.filePath,
)
}
func rollbackChanges(staged []stagedChange, ops transactionFileOps) []error {
failures := make([]error, 0)
for index := len(staged) - 1; index >= 0; index-- {
item := &staged[index]
if !item.committed && !item.originalCaptured {
continue
}
var err error
switch {
case item.committed && !item.change.remove:
err = rollbackWrittenFile(item, ops)
default:
err = restoreOriginalFile(item, ops)
}
if err != nil {
failures = append(failures, err)
}
}
return failures
}
func rollbackWrittenFile(item *stagedChange, ops transactionFileOps) error {
discardPath, err := reserveStagingPath(
filepath.Dir(item.change.filePath),
filepath.Base(item.change.filePath),
".discard",
)
if err != nil {
return preserveOriginalAfterRollbackFailure(
item,
fmt.Errorf("reserve recovery path for %s: %w", item.change.label, err),
)
}
if err := ops.moveNoReplace(item.change.filePath, discardPath); err != nil {
return preserveOriginalAfterRollbackFailure(
item,
fmt.Errorf("capture committed %s for rollback: %w", item.change.label, err),
)
}
captured, readErr := readManagedFile(
discardPath,
"captured committed "+item.change.label,
maxManagedSourceBytes,
)
if readErr != nil || !snapshotMatches(
captured,
true,
item.change.next,
item.committedInfo,
item.change.mode,
) {
cause := readErr
if cause == nil {
cause = fmt.Errorf(
"%s changed at the atomic rollback boundary: %s",
item.change.label,
item.change.filePath,
)
}
restoreErr := ops.moveNoReplace(discardPath, item.change.filePath)
if restoreErr != nil {
cause = errors.Join(
cause,
fmt.Errorf(
"restore concurrently captured %s: %w; captured object preserved at %s",
item.change.label,
restoreErr,
discardPath,
),
)
} else if captured != nil {
restored, inspectErr := readManagedFile(
item.change.filePath,
"restored concurrent "+item.change.label,
maxManagedSourceBytes,
)
if inspectErr != nil || !snapshotMatches(
restored,
true,
captured.contents,
captured.info,
captured.mode,
) {
preserveErr := ops.moveNoReplace(item.change.filePath, discardPath)
preservation := fmt.Errorf(
"changed concurrent object preserved at %s",
discardPath,
)
if preserveErr != nil {
preservation = fmt.Errorf(
"preserve changed concurrent %s: %w; objects left at %s and %s",
item.change.label,
preserveErr,
item.change.filePath,
discardPath,
)
}
cause = errors.Join(
cause,
inspectErr,
preservation,
)
}
}
return preserveOriginalAfterRollbackFailure(item, cause)
}
item.committed = false
var restoreErr error
if item.change.existed {
restoreErr = restoreOriginalFile(item, ops)
}
cleanupErr := removeVerifiedStagedFile(
discardPath,
"captured committed "+item.change.label,
item.change.next,
item.committedInfo,
item.change.mode,
)
return errors.Join(restoreErr, cleanupErr)
}
func restoreOriginalFile(item *stagedChange, ops transactionFileOps) error {
if !item.originalCaptured {
return nil
}
path := item.rollbackPath
if err := ops.moveNoReplace(path, item.change.filePath); err != nil {
cause := fmt.Errorf(
"restore %s at %s without replacing another file: %w",
item.change.label,
item.change.filePath,
err,
)
current, inspectErr := readManagedFile(
path,
"rollback "+item.change.label,
maxManagedSourceBytes,
)
if inspectErr == nil && snapshotMatches(
current,
true,
item.change.original,
item.change.originalInfo,
item.change.originalMode,
) {
return preserveRollbackFile(item, cause)
}
return preserveChangedRollback(item, errors.Join(cause, inspectErr))
}
return finishOriginalRestore(item, ops)
}
func finishOriginalRestore(item *stagedChange, ops transactionFileOps) error {
current, err := readManagedFile(
item.change.filePath,
item.change.label,
maxManagedSourceBytes,
)
if err == nil && snapshotMatches(
current,
true,
item.change.original,
item.change.originalInfo,
item.change.originalMode,
) {
item.rollbackPath = ""
item.originalCaptured = false
item.committed = false
return nil
}
cause := err
if cause == nil {
cause = fmt.Errorf(
"rollback %s changed at the atomic restore boundary: %s",
item.change.label,
item.change.filePath,
)
}
path := item.rollbackPath
if restoreErr := ops.moveNoReplace(item.change.filePath, path); restoreErr != nil {
item.rollbackPath = ""
item.originalCaptured = false
return errors.Join(
cause,
fmt.Errorf(
"preserve changed rollback object: %w; objects left at %s and %s",
restoreErr,
item.change.filePath,
path,
),
)
}
item.rollbackPath = ""
item.originalCaptured = false
return fmt.Errorf("%w; changed rollback object preserved at %s", cause, path)
}
func preserveChangedRollback(item *stagedChange, cause error) error {
path := item.rollbackPath
item.rollbackPath = ""
item.originalCaptured = false
return fmt.Errorf("%w; changed rollback object preserved at %s", cause, path)
}
func preserveOriginalAfterRollbackFailure(item *stagedChange, cause error) error {
if !item.change.existed || !item.originalCaptured {
return cause
}
return preserveRollbackFile(item, cause)
}
func preserveRollbackFile(item *stagedChange, cause error) error {
if item.rollbackPath == "" {
return cause
}
path := item.rollbackPath
current, err := readManagedFile(
path,
"rollback "+item.change.label,
maxManagedSourceBytes,
)
if err != nil || !snapshotMatches(
current,
true,
item.change.original,
item.change.originalInfo,
item.change.originalMode,
) {
return preserveChangedRollback(
item,
errors.Join(cause, err, fmt.Errorf("rollback %s changed", item.change.label)),
)
}
item.rollbackPath = ""
item.originalCaptured = false
return fmt.Errorf("%w; original content preserved at %s", cause, path)
}