7 Commits
Author SHA1 Message Date
ai-ag2026andClaude Fable 5 0dc16dcce8 fix(config): restore read-only-target protection for atomic writes
Review finding 2 (10.07 gate): a deliberately locked config (0444) in a
writable directory was silently overwritten — atomic replace creates a
fresh temp inode and renames over the read-only file, defeating the old
in-place Path.write_text PermissionError contract.

Probe the existing target with a non-truncating O_WRONLY open before any
replacement work and let the PermissionError propagate. The probe fstat()s
the fd it actually opened and hands that stat to the rest of the write, so
a concurrent writer replacing the inode between stat and probe refreshes
the metadata instead of failing (keeps concurrent-writer semantics).

Regression: writable parent + 0444 target now raises and keeps the
original bytes and mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:32:56 +02:00
ai-ag2026 83bd7125bd fix: avoid racy umask probe for config writes 2026-07-18 13:47:04 +02:00
ai-ag2026 2706433802 fix: preserve config extended attributes 2026-07-18 13:47:04 +02:00
ai-ag2026 297535bfba Preserve hard-linked configs during atomic writes 2026-07-18 13:47:04 +02:00
ai-ag2026 c7ce0db4fa Handle unsupported ownership transfer in atomic writes 2026-07-18 13:47:04 +02:00
ai-ag2026 d5904283ab Cover onboarding with atomic config writes 2026-07-18 13:47:04 +02:00
ai-ag2026andClaude Opus 4.8 828b244031 Atomic config.yaml writes to survive mid-write crashes
config.yaml and profile config.yaml were persisted with a plain
Path.write_text(), which truncates the target before writing.  A crash
(or exception) after the truncate but before the full payload was
flushed left the live config truncated/corrupt, so the next agent/WebUI
start failed to parse it (availability regression).

Extract a shared api.paths._atomic_write_text() helper (tempfile in the
same dir -> write -> flush + os.fsync -> os.replace; unlink tmp on
error), mirroring the existing .env / cost-snapshot atomic pattern in
api.providers, and apply it to _save_yaml_config_file (api.config) and
the two profile model-config writers (api.profiles).  On any mid-write
failure os.replace never runs, so the original file stays byte-for-byte
intact.

Preserve the target's permissions: tempfile.mkstemp() hard-codes 0600
and os.replace carries the temp file's mode onto the target, so without
an explicit chmod every save would silently tighten a group/other-
readable config.yaml (the homelab install ships 0644, profiles 0664)
down to owner-only.  Copy the existing file's mode before the replace,
falling back to the umask-adjusted 0666 for a new file.  config.yaml
holds no secrets, so that tightening would be a regression, not
hardening (unlike .env, which stays 0600 in api.providers).

settings.json is intentionally left untouched here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:47:04 +02:00