JuliusBrussee/caveman · error
: : inspect database parent
Error message
%w: %w: inspect database parent: %v
What it means
inspectSQLiteGeneration (engine/ccr/store_generation.go:43) verifies the identity of the SQLite database and its parent directory before trusting the recovery store. When resolving symlinks of the database's parent directory fails for any reason other than 'does not exist' (e.g. EACCES, ELOOP, I/O errors), it wraps the failure with both ErrStorageChanged and errStorageUnverifiable. The errStorageUnverifiable marker signals the store could NOT confirm the database was replaced — only that it cannot be verified right now — so terminal quarantine decisions that match on errStorageUnverifiable can distinguish this from a confirmed swap (line 46).
Solutions
- Check and restore execute/search permission on the database's parent directory and all path components (chmod, or run as the owning user).
- Inspect the wrapped %v error: EACCES means permissions, ELOOP means a symlink cycle — fix the specific filesystem condition.
- Verify ~/.caveman (or the configured CCR dir) is a real directory on a local filesystem, not a symlink loop or flaky network mount.
- Ensure the path passed to the store came from PrepareSQLitePathCanonical; non-canonical spellings are intentionally rejected.
- If this occurs at startup only, confirm the parent directory exists before opening the store (a missing parent takes the different 'database parent changed' path at line 46).
Example fix
// before // ~/.caveman owned by root; engine runs as service user → EvalSymlinks fails with EACCES // error: storage changed: storage identity could not be verified: inspect database parent: permission denied // after sudo chown -R serviceuser:servicegroup ~/.caveman chmod 700 ~/.caveman
Defensive patterns
Strategy: try-catch
Validate before calling
func ccrParentAccessible(path string) error {
parent := filepath.Dir(path)
info, err := os.Stat(parent)
if err != nil {
return fmt.Errorf("ccr parent %s: %w", parent, err)
}
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", parent)
}
if _, err := os.Stat(filepath.Join(parent, ".probe")); os.IsPermission(err) {
return fmt.Errorf("no search permission on %s", parent)
}
return nil
}
// call before opening the store
Try / catch
files, err := inspectSQLiteGeneration(path)
if err != nil {
if errors.Is(err, ErrStorageChanged) && errors.Is(err, errStorageUnverifiable) {
// cannot verify right now (permissions/IO) — do NOT quarantine the store;
// surface a retryable environment error
return fmt.Errorf("ccr store temporarily unverifiable, fix permissions/mount and retry: %w", err)
}
return err
} Prevention
- Ensure the service user has execute permission on every component of the CCR path.
- Avoid symlink loops in home directories managed by dotfile tools.
- Run the store on local filesystems, not flaky NFS/network mounts.
- Distinguish errStorageUnverifiable (transient) from a confirmed ErrStorageChanged before making terminal decisions.
- Pre-create and chown the CCR directory during deployment, before first engine run.
When it happens
Trigger: checkGeneration (and its callers openWithBudgetHooks / secureSQLiteFiles / the generation-capture test) invoke inspectSQLiteGeneration and filepath.EvalSymlinks(parent) fails with an error other than os.ErrNotExist — e.g. permission denied on a component of the parent path, too many symlink levels, or a transient filesystem I/O error.
Common situations: Running the engine under a service account that lost execute permission on ~/.caveman or an intermediate directory; a symlink chain in the CCR path (common with dotfile managers or mounted home dirs) creating a loop; containerized runs where the data volume is mounted with restrictive modes; NFS/overlayfs transient failures.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- sqlite parent is group/world writable
- : database parent changed
- inspect sqlite parent ACL
- inspect sqlite parent
- native session key chmod dir
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/0e6e9a04d2aa3fb3.
Report an issue: GitHub.
Appendix: source
Thrown at engine/ccr/store_generation.go:43
// a parent directory whose mode was loosened. It still wraps ErrStorageChanged,
// so a caller that only asks "is this store usable right now" is unaffected —
// only the terminal quarantine decision looks for it.
var errStorageUnverifiable = errors.New("storage identity could not be verified")
// inspectSQLiteGeneration requires the canonical path PrepareSQLitePathCanonical
// returned. The parent check below is a re-verification that no component became
// a symlink since; it compares spellings on purpose, because following a swapped
// intermediate symlink yields the same directory identity and so cannot be
// detected by os.SameFile. A non-canonical spelling is reported as a change.
func inspectSQLiteGeneration(path string) (sqliteGeneration, error) {
var files sqliteGeneration
if path == ":memory:" {
return files, nil
}
parent := filepath.Dir(path)
resolved, err := filepath.EvalSymlinks(parent)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return files, fmt.Errorf("%w: %w: inspect database parent: %v", ErrStorageChanged, errStorageUnverifiable, err)
}
if err != nil || resolved != parent {
return files, fmt.Errorf("%w: database parent changed", ErrStorageChanged)
}
info, err := os.Stat(parent)
if errors.Is(err, os.ErrNotExist) {
return files, fmt.Errorf("%w: database parent changed", ErrStorageChanged)
}
if err != nil {
return files, fmt.Errorf("%w: %w: inspect database parent: %v", ErrStorageChanged, errStorageUnverifiable, err)
}
if err := validateSQLiteParentSecurity(parent, info); err != nil {
return files, fmt.Errorf("%w: %w: %v", ErrStorageChanged, errStorageUnverifiable, err)
}
for i, suffix := range sqliteSuffixes {
info, err := inspectSQLiteFile(path + suffix)
if errors.Is(err, os.ErrNotExist) && i != 0 {
continueView on GitHub (pinned to 3ee70a1026)