Lock the state directory and write vault files atomically (closes #34)
check / check (push) Successful in 1m31s

Each command that changes the state directory holds one lock: flock(2)
on `lock` in the state directory, dropped by the kernel if the process
dies, or a process-wide mutex on the in-memory test filesystem. It
covers the state directory, not each vault, because `currentvault`,
`vault create` and cross-vault moves span vaults, and a lock file in a
vault would be deleted by `vault remove` under a waiting command.

Files go through `secret.WriteFileAtomic`; versions, new secrets and
cross-vault copies are built in a temporary directory and renamed into
place; removals rename out of the way first. Left for later: replacing
an unlocker (#71) and deleting
what an interrupted command leaves under a `.tmp-` name
(#75).

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #69.
This commit is contained in:
2026-10-04 04:57:58 +02:00
committed by clawbot
parent bdb1c7ec18
commit 32a61ff963
24 changed files with 1906 additions and 204 deletions
+6 -7
View File
@@ -172,7 +172,7 @@ func (p *PGPUnlocker) GetID() string {
func (p *PGPUnlocker) Remove() error {
// For PGP unlockers, we just need to remove the directory
// No external resources (like keychain items) to clean up
err := p.fs.RemoveAll(p.Directory)
err := RemoveDirAtomic(p.fs, p.Directory)
if err != nil {
return fmt.Errorf("failed to remove PGP unlocker directory: %w", err)
}
@@ -275,7 +275,7 @@ func CreatePGPUnlocker(
ageRecipient := ageIdentity.Recipient().String()
recipientPath := filepath.Join(unlockerDir, "pub.txt")
err = afero.WriteFile(fs, recipientPath, []byte(ageRecipient), FilePerms)
err = WriteFileAtomic(fs, recipientPath, []byte(ageRecipient))
if err != nil {
return nil, fmt.Errorf("failed to write age recipient: %w", err)
}
@@ -298,7 +298,7 @@ func CreatePGPUnlocker(
// Write encrypted long-term private key
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
err = afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms)
err = WriteFileAtomic(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge)
if err != nil {
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
}
@@ -315,7 +315,7 @@ func CreatePGPUnlocker(
agePrivKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
err = afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms)
err = WriteFileAtomic(fs, agePrivKeyPath, encryptedAgePrivKey)
if err != nil {
return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
}
@@ -357,9 +357,8 @@ func writePGPUnlockerMetadata(
return nil, fmt.Errorf("failed to marshal unlocker metadata: %w", err)
}
err = afero.WriteFile(fs,
filepath.Join(unlockerDir, "unlocker-metadata.json"),
metadataBytes, FilePerms)
err = WriteFileAtomic(fs,
filepath.Join(unlockerDir, "unlocker-metadata.json"), metadataBytes)
if err != nil {
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
}