Documentation
¶
Overview ¶
Package safeio centralizes the "never delete or overwrite a user artifact without a recovery path" primitives that agent-deck grew piecemeal after a recurring class of data-loss incidents (the 2026-06-04 profile-index wipe and its 2025-12-11 / 2026-04-17 predecessors).
Three bespoke guards previously lived at three sites:
- #1383/S1+S2 — SaveInstances refused an empty-payload sweep and backed up the DB before a large drop; SaveUserConfig refused dropping populated sections and backed up config.toml before the atomic rename.
- #1429 — the conductor dir relocation copied + verified meta.json before RemoveAll'ing the source, refusing to delete the only copy.
- #1449 — worktree teardown refused to remove a worktree still referenced by a sibling session.
safeio distills those into two composable primitives:
SafeOverwrite — backup → atomic durable write, with pluggable refusal guards
(refuse-empty, refuse-section-drop) checked BEFORE any mutation.
SafeRemove — refuse to delete a path that is still referenced / is the only
copy (via a pluggable predicate), so a destructive RemoveAll is
structurally guarded rather than guarded ad-hoc at each caller.
The package depends only on the standard library and internal/atomicfile, so any internal package can adopt it without an import cycle.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrRefusingEmptyOverwrite = errors.New("safeio: refusing to overwrite a populated file with empty content")
ErrRefusingEmptyOverwrite is returned by SafeOverwrite when Options.RefuseEmpty is set and the call would replace a NON-empty file with empty bytes. An empty payload over an already-empty (or absent) file is allowed — only the destructive empty-over-populated case is refused.
var ErrStillReferenced = errors.New("safeio: refusing to remove a still-referenced path")
ErrStillReferenced is returned by SafeRemove when Options.StillReferenced reports the path is in use (e.g. a worktree shared by a sibling session, or a source whose only copy this would delete).
Functions ¶
func Backup ¶
Backup copies an existing file to "<path>.bak" via a temp-file + rename so the .bak is never torn. It returns the backup path on success, "" when the source does not exist (a no-op), or an error. The backup is written 0o600 to keep it as private as the originals it protects (config.toml / state.db may carry session metadata).
func SafeOverwrite ¶
SafeOverwrite replaces path's contents with newBytes, guarded against the destructive-write footguns. Order of operations:
- Read the existing content (nil if the file is new).
- Run Options.Guard(old, new) — refuse with the guard's error, untouched.
- RefuseEmpty: refuse empty-over-populated with ErrRefusingEmptyOverwrite.
- Back up the existing file to <path>.bak (unless SkipBackup or new file).
- Atomically + durably write newBytes (temp → fsync → rename → dir fsync).
All refusals happen BEFORE any backup or write, so a refused call leaves both the original and any prior .bak exactly as they were.
func SafeRemove ¶
func SafeRemove(path string, opts RemoveOptions) error
SafeRemove removes path (file or directory tree) only when it is safe to do so. It refuses an empty path (guarding against a stray RemoveAll("")), is a no-op on a non-existent path, and consults Options.StillReferenced before deleting — refusing with ErrStillReferenced when the path is in use.
Types ¶
type Options ¶
type Options struct {
// Perm is the file mode for the written file. Defaults to 0o600 when zero,
// matching the private treatment of config.toml / state.db backups.
Perm os.FileMode
// RefuseEmpty refuses to replace a non-empty file with empty bytes. This is
// the generalization of the SaveInstances empty-sweep guard for file writes:
// a stray empty payload over a populated artifact is almost always a bug.
RefuseEmpty bool
// Guard, when non-nil, is called with (oldContent, newContent) BEFORE any
// backup or write. A non-nil error aborts the overwrite with the file
// untouched and no backup made. oldContent is nil when the target does not
// exist yet. This is the extension point for caller-specific refusals such
// as the SaveUserConfig section-drop guard. The error is returned verbatim
// (wrap with %w upstream for errors.Is matching).
Guard func(oldContent, newContent []byte) error
// SkipBackup disables the pre-write .bak copy. Off by default — the backup
// is the recovery net the whole package exists to provide.
SkipBackup bool
}
Options configures SafeOverwrite.
type RemoveOptions ¶
type RemoveOptions struct {
// StillReferenced, when non-nil, is consulted before removal. Returning
// (true, reason) refuses the removal with ErrStillReferenced (reason is
// folded into the error message). This generalizes the #1449 sibling-use
// guard and the #1429 "destination still aliases the source" defense: the
// caller supplies the reference check; safeio enforces the refusal.
StillReferenced func(path string) (referenced bool, reason string)
}
RemoveOptions configures SafeRemove.