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>