aion-server 0.26.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
//! Boot-side config self-healing: the standard place where every required
//! field is checked and, when absent, inserted with its declared default.
//!
//! Ruled 2026-08-25 (Tom): "it just needs to check to make sure all the
//! default required fields are there, like every one of them, and insert them
//! if they're not. There's just got to be a standard place in the code where
//! that happens." This module is that place, and [`heal_boot_config`] is the
//! pass — run by `load_or_scaffold` on every server boot, before the merged
//! load parses and validates, against exactly the file that load will read.
//!
//! The required set is never enumerated here: the heal runs
//! [`crate::state::boot_required_probe`] — the SAME requirement functions the
//! boot path applies — and maps each refusal to its declared default through
//! [`crate::state::BOOT_REQUIRED_FIELD_DEFAULTS`], the one table co-located
//! with those functions. A requirement whose refusal has no table row simply
//! stays unhealed and refuses the boot with its own message, exactly as
//! before the heal existed — and the census in `state::boot_required` makes
//! joining a declared list a condition of adding such a constant at all.
//!
//! Contract, in full:
//!
//! - A config missing NOTHING passes through byte-untouched: no rewrite, no
//!   reformat, no backup file minted, and no line added to the boot log —
//!   the heal's shadow overlay returns its retired-variable notices as data
//!   for the authoritative loader overlay to log once, so a complete
//!   config's boot log is unchanged by this pass existing. The heal only
//!   ever opens the file for writing after it has planned at least one
//!   insertion.
//! - A symlinked config is healed at its TARGET: the link is resolved first,
//!   the backup lands beside the target, and the link itself survives — a
//!   rename over the link would silently disconnect the operator's managed
//!   source. A link whose target cannot be resolved refuses the boot naming
//!   both ends.
//! - WHICH requirements apply is decided from the RESOLVED view (the file
//!   with the environment's `store.backend` overlaid): `AION_STORE_BACKEND`
//!   can select haematite over the file's word, and the requirements follow
//!   the backend the server will actually run. WHAT is absent is decided
//!   from the file alone — the file must stand on its own, so a field the
//!   environment happens to supply is still minted into the file (with a
//!   warning that the running value differs; see below).
//! - Only ABSENCE is healed. A present value the requirement function refuses
//!   (an explicit zero) is the operator's own value: the heal leaves it and
//!   the boot refuses with the requirement's message, as it always has.
//! - The previous file is preserved beside the healed one as
//!   `<name>.pre-<version>` (`<version>` = the server version doing the
//!   healing — the version the preserved bytes predate). An existing backup
//!   is never overwritten: the next free `<name>.pre-<version>.N` is claimed
//!   with `create_new`, so no race and no silent clobber.
//! - Each inserted field is logged BY NAME with its inserted value, and a
//!   summary line names the count; the startup banner carries the count too.
//!   When an `AION_*` override is set for a field being minted, the boot
//!   WARNS naming the variable, the file value minted, and the effective
//!   runtime value — the file is healed for its own sake, but the log never
//!   asserts the file value as the running fact.
//! - A heal that cannot write refuses the boot loudly, naming the write error
//!   and every missing field. The server never boots on a config it knows is
//!   missing required fields but could not heal.
//! - Inserted fields carry the shipped configs' teaching comments
//!   (`toml_edit` preserves the rest of the file verbatim), plus an
//!   `# added by aion <version> config heal` marker line. The one place a
//!   comment cannot be attached is a key inside an INLINE table (TOML has no
//!   comment syntax there); the value is still inserted and logged.
//! - The file's dominant line ending is preserved: a CRLF document is written
//!   back CRLF, inserted lines included.
//! - A file the merged loader would refuse anyway — unreadable, not UTF-8,
//!   unparsable, an environment overlay the loader will also refuse — is left
//!   alone: the load that follows moments later refuses with its canonical
//!   message, and the heal does not word the same failure twice. VALIDATION
//!   refusals are deliberately NOT in that step-aside list (reviewed, ruled
//!   acceptable): the heal judges the file unresolved and cannot run the
//!   merged validation, so a config the loader will refuse at validation —
//!   an unsafe `store.data_dir`, say — is still healed first. Nothing is
//!   lost: only genuinely-absent fields are written, the backup preserves
//!   the original, and the boot still refuses loudly with the validator's
//!   own message. The heal does not pretend to step aside where it cannot.
//!
//! # Why the heal runs BEFORE the home writer-lock claim (reviewed, decided)
//!
//! The heal cannot run after any lock, because every lock is downstream of
//! the config being healed: the home CLAIM (`run.rs`'s `claim_home_at_birth`)
//! needs the config's listen addresses to decide whether a live holder
//! collides with this boot, and the store writer lock lives under
//! `store.data_dir` — both need the load that must already see the healed
//! file. (The home DIRECTORY itself is env-resolved — `aion_home()` reads
//! only `AION_HOME`/`HOME`, never the config, and the heal calls it before
//! reading anything — but the claim on that home cannot exist before the
//! loaded addresses do.) Two boots racing
//! the heal are therefore possible and safe by construction:
//! the staging file is a `create_new` claim on a unique name (pid + attempt),
//! backups are `create_new` claims on distinct names, and both racers mint
//! the SAME healed content from the same original — the loser of the later
//! lock claim leaves at most a spare backup, which is clutter, not damage.

use std::io::Write as _;
use std::path::{Path, PathBuf};

use tracing::{info, warn};

use crate::error::ServerError;
use crate::state::{
    BOOT_REQUIRED_FIELD_DEFAULTS, NOT_UPGRADE_HEALABLE, RequiredFieldDefault, boot_required_probe,
};

use super::{CliOverrides, ServerConfig, aion_home, env, file, home};

/// The version stamped into heal marker comments and backup file names — the
/// version whose boot performed the heal, i.e. the version the preserved
/// backup predates.
const HEAL_VERSION: &str = env!("CARGO_PKG_VERSION");

/// One field the heal inserted: its dotted config path and the TOML value
/// literal written into the file.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct InsertedField {
    /// The dotted config path, e.g. `observability.max_batch_events`.
    pub(crate) path: &'static str,
    /// The inserted value, as the TOML literal written into the file.
    pub(crate) value: &'static str,
}

/// What the boot-side config heal did: nothing (the common case — the config
/// was complete, or there was no config file at all), or a written heal with
/// the file, its preserved predecessor, and every inserted field.
#[derive(Debug, Default)]
pub(crate) struct HealOutcome {
    /// The healed config file — the resolved TARGET when discovery found a
    /// symlink — when a heal was written.
    pub(crate) config_path: Option<PathBuf>,
    /// Where the previous file's bytes were preserved, when a heal was written.
    pub(crate) backup_path: Option<PathBuf>,
    /// Every field the heal inserted, in insertion order. Empty means the
    /// file was left byte-untouched.
    pub(crate) inserted: Vec<InsertedField>,
}

/// Reconcile the config file a boot is about to load against the boot path's
/// required-field set, healing absences in place — the standard boot-side
/// pass. Discovers the SAME file the merged load discovers (explicit
/// `--config`, then project-local `./aion.toml`, then `<AION_HOME>/config.toml`)
/// and logs what it healed.
///
/// # Errors
///
/// Returns [`ServerError::Config`] when discovery context cannot be resolved
/// (home, working directory, a `--config` tilde that cannot expand), when a
/// discovered symlink's target cannot be resolved, or when a needed heal
/// cannot be written — the boot must not proceed on a config known to be
/// missing required fields.
pub(crate) fn heal_boot_config(cli: &CliOverrides) -> Result<HealOutcome, ServerError> {
    let aion_home = aion_home()?;
    let working_dir = std::env::current_dir().map_err(|source| ServerError::Config {
        message: format!("failed to resolve the current directory for config discovery: {source}"),
    })?;
    let explicit = cli
        .config_path
        .as_deref()
        .map(home::expand_tilde)
        .transpose()?;
    let env_vars: Vec<(String, String)> = std::env::vars().collect();
    heal_discovered(
        explicit.as_deref(),
        &aion_home.path,
        &working_dir,
        &env_vars,
    )
}

/// [`heal_boot_config`] with the discovery context and environment named by
/// the caller — the same injection seam the loader's `load_for_test` and the
/// env overlay's `overlay_vars` provide.
///
/// # Errors
///
/// Returns [`ServerError::Config`] exactly as [`heal_boot_config`] does.
pub(super) fn heal_discovered(
    explicit: Option<&Path>,
    aion_home: &Path,
    working_dir: &Path,
    env_vars: &[(String, String)],
) -> Result<HealOutcome, ServerError> {
    match file::discover_path(explicit, aion_home, working_dir)? {
        Some((path, _source)) => {
            let outcome = heal_config_file(&path, env_vars)?;
            log_outcome(&outcome, env_vars);
            Ok(outcome)
        }
        None => Ok(HealOutcome::default()),
    }
}

/// Heal one config file in place. See the module docs for the full contract;
/// this is the whole mechanism, in order: resolve the link target, read,
/// parse, overlay the environment's backend for applicability, probe (fast
/// path out when complete), plan insertions by driving the probe to a fixed
/// point ([`plan_and_insert`]), back up, write.
///
/// # Errors
///
/// Returns [`ServerError::Config`] when a symlink's target cannot be
/// resolved, or when insertions are needed and the healed file (or its
/// backup) cannot be written, naming the failure and every missing field.
pub(super) fn heal_config_file(
    path: &Path,
    env_vars: &[(String, String)],
) -> Result<HealOutcome, ServerError> {
    let Some(target) = resolve_link_target(path)? else {
        return Ok(HealOutcome::default());
    };
    let Some((original, text)) = readable_utf8(&target) else {
        return Ok(HealOutcome::default());
    };
    let Ok(file_view) = ServerConfig::parse_unresolved(&original) else {
        // Unparsable: the merged load refuses this file moments later with
        // its canonical message naming the source; the heal steps aside.
        return Ok(HealOutcome::default());
    };
    // WHICH requirements apply follows the RESOLVED config: the environment
    // can overlay `store.backend` (AION_STORE_BACKEND), and boot gates the
    // node-cache-budget requirement on the backend the server will RUN, not
    // the one the file names. WHAT is absent still follows the file alone,
    // so only the backend fact is lifted from the overlaid view. This is a
    // SHADOW evaluation of the loader's own overlay (one parser, not two):
    // the retired-variable notices it returns are deliberately discarded —
    // the loader's authoritative overlay logs them once, moments later — so
    // this pass adds no line to any boot's log, byte-untouched boots included.
    let mut resolved_view = file_view.clone();
    if env::overlay_vars(&mut resolved_view, env_vars.iter().cloned()).is_err() {
        // An environment value the loader will refuse identically moments
        // later; the heal steps aside rather than wording it twice.
        return Ok(HealOutcome::default());
    }
    let mut working = file_view;
    working.store.backend = resolved_view.store.backend;
    if boot_required_probe(&working).is_ok() {
        // Complete config: byte-untouched by construction — nothing below
        // this line runs, no write handle is ever opened.
        return Ok(HealOutcome::default());
    }
    let mut document: toml_edit::DocumentMut = match text.parse() {
        Ok(document) => document,
        Err(error) => {
            // `toml` parsed what `toml_edit` cannot: the file cannot be
            // edited in place, and required fields are known to be missing
            // (the probe just refused). Refuse the boot naming both.
            return Err(heal_refusal(
                &target,
                &absent_fields(&working),
                &format!("the file could not be re-parsed for comment-preserving editing: {error}"),
            ));
        }
    };
    let inserted = plan_and_insert(&target, &mut document, &working)?;
    if inserted.is_empty() {
        // The probe refused but nothing was ABSENT (an explicit zero, or a
        // requirement with no declared default): the operator's value stands
        // and the boot will refuse with the requirement's own message.
        return Ok(HealOutcome::default());
    }
    let healed = restore_line_endings(&text, document.to_string());
    let backup_path = write_backup(&target, &original)
        .map_err(|detail| heal_refusal(&target, &field_names(&inserted), &detail))?;
    write_replace(&target, healed.as_bytes())
        .map_err(|detail| heal_refusal(&target, &field_names(&inserted), &detail))?;
    Ok(HealOutcome {
        config_path: Some(target),
        backup_path: Some(backup_path),
        inserted,
    })
}

/// The file the heal must actually edit: the canonical TARGET when `path` is
/// (or traverses) a symlink, `path` itself otherwise, or `None` when nothing
/// exists there (the merged load owns that refusal). Healing the link's name
/// with a rename would replace the LINK with a regular file — the backup
/// would land beside the link, the real target would stay unhealed, and the
/// operator's managed source would be silently disconnected.
///
/// # Errors
///
/// Returns [`ServerError::Config`] when the path exists but cannot be
/// canonicalized — a link whose target is missing or unresolvable — naming
/// the link and, where readable, its target.
fn resolve_link_target(path: &Path) -> Result<Option<PathBuf>, ServerError> {
    let Ok(metadata) = std::fs::symlink_metadata(path) else {
        return Ok(None);
    };
    match std::fs::canonicalize(path) {
        Ok(target) => {
            if metadata.file_type().is_symlink() {
                info!(
                    link = %path.display(),
                    target = %target.display(),
                    "config heal resolved the discovered config link to its target"
                );
            }
            Ok(Some(target))
        }
        Err(error) => {
            let named_target = std::fs::read_link(path)
                .map_or_else(|_| "unresolvable".to_owned(), |t| t.display().to_string());
            Err(ServerError::Config {
                message: format!(
                    "config heal cannot resolve `{}`: {error}. The path is a link whose \
                     target (`{named_target}`) cannot be reached, so neither the heal nor \
                     the load that follows can act on it; fix or remove the link",
                    path.display()
                ),
            })
        }
    }
}

/// The file's bytes and UTF-8 view, or `None` when the merged load would
/// refuse the file anyway (unreadable or not UTF-8) and owns that refusal.
fn readable_utf8(path: &Path) -> Option<(Vec<u8>, String)> {
    let bytes = std::fs::read(path).ok()?;
    let text = std::str::from_utf8(&bytes).ok()?.to_owned();
    Some((bytes, text))
}

/// Restore the original document's dominant line ending on the healed text.
/// `toml_edit` emits `\n` throughout, which would silently rewrite every line
/// of a CRLF document; when CRLF dominates the ORIGINAL, the healed output —
/// inserted lines included — is folded back to CRLF. An LF-dominant or
/// single-line document passes through as emitted.
fn restore_line_endings(original: &str, healed: String) -> String {
    let crlf = original.matches("\r\n").count();
    let lf_only = original.matches('\n').count() - crlf;
    if crlf > lf_only {
        healed.replace("\r\n", "\n").replace('\n', "\r\n")
    } else {
        healed
    }
}

/// Drive the requirement functions to a fixed point: probe, look the refusal
/// up in the declared table, satisfy the field, re-probe.
///
/// [`boot_required_probe`] stops at its FIRST refusal, so each pass satisfies
/// one field on the in-memory `working` copy to let the probe surface the
/// next requirement. Only an ABSENT field earns a document insertion; a
/// present-but-refused value (an explicit zero) is stepped over in memory —
/// never written anywhere — so absences BEHIND it still heal while the boot
/// keeps refusing the operator's value. Terminates because each table row is
/// stepped over at most once and any refusal outside the table stops the
/// loop (the boot surfaces that refusal unchanged).
///
/// When insertions were made, the edited document is re-parsed and every
/// inserted field is verified present where the loader reads it — a heal
/// that mangled the document refuses before anything touches disk.
fn plan_and_insert(
    path: &Path,
    document: &mut toml_edit::DocumentMut,
    applicable_view: &ServerConfig,
) -> Result<Vec<InsertedField>, ServerError> {
    let mut working = applicable_view.clone();
    let mut stepped_over: Vec<&'static str> = Vec::new();
    let mut inserted: Vec<InsertedField> = Vec::new();
    // The `while let` ends when the probe passes — or on the refusal shapes
    // the planner has no business interpreting (a non-config error, which
    // none of the requirement functions produces today): either way the
    // fixed point is reached and the boot surfaces whatever remains.
    while let Err(ServerError::Config { message }) = boot_required_probe(&working) {
        let Some(entry) = BOOT_REQUIRED_FIELD_DEFAULTS
            .iter()
            .find(|entry| entry.requirement_message == message)
        else {
            // A required field with no declared default: unhealable, and
            // the boot will refuse with this exact message. The census in
            // `state::boot_required` guarantees such a field is DECLARED
            // not-upgrade-healable, never merely forgotten — say so, with
            // the declared reason, so the log shows the heal saw it and
            // deliberately stood aside.
            if let Some((_, reason)) = NOT_UPGRADE_HEALABLE
                .iter()
                .find(|(unhealable, _)| *unhealable == message)
            {
                info!(
                    reason,
                    "config heal: the refused requirement is declared \
                     not-upgrade-healable; the boot refuses with the \
                     requirement's own message"
                );
            }
            break;
        };
        if stepped_over.contains(&entry.path) {
            // The refusal repeats after the field was satisfied: no
            // progress, stop rather than loop.
            break;
        }
        let absent = (entry.is_absent)(&working);
        (entry.satisfy_in_memory)(&mut working);
        stepped_over.push(entry.path);
        if absent {
            insert_field(document, entry)
                .map_err(|detail| heal_refusal(path, &[entry.path.to_owned()], &detail))?;
            inserted.push(InsertedField {
                path: entry.path,
                value: entry.default_toml,
            });
        }
    }
    if !inserted.is_empty() {
        let edited = document.to_string();
        let reparsed = ServerConfig::parse_unresolved(edited.as_bytes()).map_err(|_| {
            heal_refusal(
                path,
                &field_names(&inserted),
                "the healed document no longer parses as a server config; \
                 nothing was written",
            )
        })?;
        for field in &inserted {
            let Some(entry) = BOOT_REQUIRED_FIELD_DEFAULTS
                .iter()
                .find(|entry| entry.path == field.path)
            else {
                continue;
            };
            if (entry.is_absent)(&reparsed) {
                return Err(heal_refusal(
                    path,
                    &field_names(&inserted),
                    &format!(
                        "{} was inserted but the loader does not see it in the \
                         healed document; nothing was written",
                        field.path
                    ),
                ));
            }
        }
    }
    Ok(inserted)
}

/// Insert one declared default into the document: the section is created when
/// absent, the value is written from the table's TOML literal, and the
/// teaching comment plus the `added by` marker are attached where the
/// mechanism allows (a standard `[section]` table; an inline table has no
/// comment syntax, so its insertion carries the value alone).
fn insert_field(
    document: &mut toml_edit::DocumentMut,
    entry: &RequiredFieldDefault,
) -> Result<(), String> {
    let value: toml_edit::Value = entry.default_toml.parse().map_err(|error| {
        format!(
            "the declared default `{}` for {} is not a TOML value: {error}",
            entry.default_toml, entry.path
        )
    })?;
    let fresh_section = document.get(entry.section).is_none();
    let item = document
        .entry(entry.section)
        .or_insert_with(toml_edit::table);
    match item {
        toml_edit::Item::Table(table) => {
            if fresh_section {
                // A blank line before the new `[section]` header, so the
                // appended section does not butt against the previous one.
                table.decor_mut().set_prefix("\n");
            }
            let mut spaced = value;
            spaced.decor_mut().set_prefix(" ");
            table.insert(entry.key, toml_edit::Item::Value(spaced));
            let Some(mut key) = table.key_mut(entry.key) else {
                return Err(format!(
                    "{} was inserted into [{}] but its key cannot be decorated",
                    entry.path, entry.section
                ));
            };
            key.leaf_decor_mut().set_prefix(comment_block(entry));
            Ok(())
        }
        toml_edit::Item::Value(toml_edit::Value::InlineTable(inline)) => {
            // TOML has no comment syntax inside an inline table; the value
            // is inserted without its teaching comment, and the boot log
            // still names the field and value. Spacing: the old LAST
            // entry's value suffix held the space before `}` — left alone
            // it would precede the new comma as a stray ` ,` — so that
            // space moves onto the new last entry, and the new entry is
            // decorated `key = value` like its neighbours.
            let previous_last = inline.iter().last().map(|(key, _)| key.to_owned());
            if let Some(previous_last) = previous_last
                && let Some(previous) = inline.get_mut(&previous_last)
            {
                previous.decor_mut().set_suffix("");
            }
            inline.insert(entry.key, value);
            if let Some(mut key) = inline.key_mut(entry.key) {
                key.leaf_decor_mut().set_prefix(" ");
                key.leaf_decor_mut().set_suffix(" ");
            }
            if let Some(spaced) = inline.get_mut(entry.key) {
                spaced.decor_mut().set_prefix(" ");
                spaced.decor_mut().set_suffix(" ");
            }
            Ok(())
        }
        other => Err(format!(
            "`{}` exists but is not a table (found {}), so {} cannot be inserted",
            entry.section,
            other.type_name(),
            entry.path
        )),
    }
}

/// The comment block written above an inserted key: the teaching comment in
/// the shipped configs' voice, then the marker naming the healing version.
fn comment_block(entry: &RequiredFieldDefault) -> String {
    let mut block = String::new();
    for line in entry.teaching_comment {
        block.push_str("# ");
        block.push_str(line);
        block.push('\n');
    }
    block.push_str("# added by aion ");
    block.push_str(HEAL_VERSION);
    block.push_str(" config heal\n");
    block
}

/// Preserve the pre-heal bytes beside the config as `<name>.pre-<version>`,
/// claiming the first free name (`.2`, `.3`, … on collision) with
/// `create_new` so an existing backup is never overwritten — silently or at
/// all — and two racing boots cannot claim the same name. The collision loop
/// is deliberately unbounded: a heal must never refuse because backups
/// accumulated, so it walks until a free name is claimed. The parent
/// directory is fsynced after the claim so the backup's existence is durable
/// before the original is replaced.
fn write_backup(path: &Path, original: &[u8]) -> Result<PathBuf, String> {
    let file_name = path
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or_else(|| format!("`{}` has no file name to back up beside", path.display()))?;
    let permissions = std::fs::metadata(path)
        .map_err(|error| format!("cannot read `{}` metadata: {error}", path.display()))?
        .permissions();
    let mut attempt: u64 = 1;
    loop {
        let candidate_name = if attempt == 1 {
            format!("{file_name}.pre-{HEAL_VERSION}")
        } else {
            format!("{file_name}.pre-{HEAL_VERSION}.{attempt}")
        };
        let candidate = path.with_file_name(&candidate_name);
        match std::fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&candidate)
        {
            Ok(mut backup) => {
                backup
                    .write_all(original)
                    .and_then(|()| backup.sync_all())
                    .map_err(|error| {
                        format!("cannot write backup `{}`: {error}", candidate.display())
                    })?;
                std::fs::set_permissions(&candidate, permissions).map_err(|error| {
                    format!(
                        "cannot set backup `{}` permissions: {error}",
                        candidate.display()
                    )
                })?;
                sync_parent_dir(&candidate)?;
                return Ok(candidate);
            }
            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
                attempt = attempt.saturating_add(1);
            }
            Err(error) => {
                return Err(format!(
                    "cannot create backup `{}`: {error}",
                    candidate.display()
                ));
            }
        }
    }
}

/// Replace the config file's contents atomically: claim a UNIQUE staging file
/// beside it with `create_new` (pid + attempt in the name; `O_EXCL` refuses a
/// planted symlink outright and a collision merely advances the attempt),
/// write and fsync the healed bytes with the original's permissions, rename
/// it over the original, and fsync the parent directory so the swap is
/// durable. The backup already preserves the prior bytes, so a failure here
/// leaves either the original or the healed file — never a torn one — and
/// the staging file is removed on every failure path.
fn write_replace(path: &Path, healed: &[u8]) -> Result<(), String> {
    let file_name = path
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or_else(|| format!("`{}` has no file name to replace", path.display()))?;
    let permissions = std::fs::metadata(path)
        .map_err(|error| format!("cannot read `{}` metadata: {error}", path.display()))?
        .permissions();
    let (staging, mut file) = claim_staging_file(path, file_name)?;
    let write_staged = |file: &mut std::fs::File| -> Result<(), String> {
        file.write_all(healed)
            .and_then(|()| file.sync_all())
            .map_err(|error| format!("cannot write `{}`: {error}", staging.display()))?;
        std::fs::set_permissions(&staging, permissions.clone())
            .map_err(|error| format!("cannot set `{}` permissions: {error}", staging.display()))?;
        std::fs::rename(&staging, path).map_err(|error| {
            format!(
                "cannot move `{}` over `{}`: {error}",
                staging.display(),
                path.display()
            )
        })?;
        sync_parent_dir(path)
    };
    let result = write_staged(&mut file);
    drop(file);
    result.map_err(|detail| match std::fs::remove_file(&staging) {
        Ok(()) => detail,
        Err(cleanup) if cleanup.kind() == std::io::ErrorKind::NotFound => detail,
        Err(cleanup) => format!(
            "{detail}; the staging file `{}` also could not be removed: {cleanup}",
            staging.display()
        ),
    })
}

/// Claim a fresh staging file beside `path` with `create_new`: the name
/// carries this process's pid and an attempt counter, so concurrent boots
/// claim distinct files and a name someone else holds — including a planted
/// symlink, which `O_EXCL` refuses without following — just advances the
/// attempt. Unbounded for the same reason as the backup loop: the heal walks
/// until it claims a name of its own.
fn claim_staging_file(path: &Path, file_name: &str) -> Result<(PathBuf, std::fs::File), String> {
    let pid = std::process::id();
    let mut attempt: u64 = 1;
    loop {
        let candidate = path.with_file_name(format!("{file_name}.heal-staging-{pid}.{attempt}"));
        match std::fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&candidate)
        {
            Ok(file) => return Ok((candidate, file)),
            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
                attempt = attempt.saturating_add(1);
            }
            Err(error) => {
                return Err(format!(
                    "cannot create staging file `{}`: {error}",
                    candidate.display()
                ));
            }
        }
    }
}

/// Fsync the directory containing `path`, so a just-created or just-renamed
/// entry is durable in the directory itself, not only in the file's blocks.
#[cfg(unix)]
fn sync_parent_dir(path: &Path) -> Result<(), String> {
    let parent = path
        .parent()
        .ok_or_else(|| format!("`{}` has no parent directory to fsync", path.display()))?;
    std::fs::File::open(parent)
        .and_then(|directory| directory.sync_all())
        .map_err(|error| format!("cannot fsync directory `{}`: {error}", parent.display()))
}

/// On non-Unix targets a directory cannot be opened as a file to fsync; the
/// rename itself is the durability point the platform offers.
#[cfg(not(unix))]
fn sync_parent_dir(_path: &Path) -> Result<(), String> {
    Ok(())
}

/// The loud boot refusal for a heal that could not complete: names the file,
/// every missing field, and the failure — the server never boots on a config
/// it knows is missing required fields but could not heal.
fn heal_refusal(path: &Path, missing: &[String], detail: &str) -> ServerError {
    let fields = if missing.is_empty() {
        "unknown (the probe refused before any field was planned)".to_owned()
    } else {
        missing.join(", ")
    };
    ServerError::Config {
        message: format!(
            "config heal failed for `{}`: {detail}. The config is missing required \
             fields ({fields}) and the server does not boot on an unhealed config; \
             fix the write path, or add the fields by hand",
            path.display()
        ),
    }
}

/// Every declared-default field structurally absent from a parsed config —
/// used only to NAME fields in a refusal raised before the probe loop could
/// plan them.
fn absent_fields(config: &ServerConfig) -> Vec<String> {
    BOOT_REQUIRED_FIELD_DEFAULTS
        .iter()
        .filter(|entry| (entry.is_absent)(config))
        .map(|entry| entry.path.to_owned())
        .collect()
}

/// The dotted paths of a set of planned insertions, for refusal messages.
fn field_names(inserted: &[InsertedField]) -> Vec<String> {
    inserted.iter().map(|field| field.path.to_owned()).collect()
}

/// Log what the heal did: each inserted field by name with its inserted
/// value, a WARNING for each minted field whose `AION_*` override rules the
/// running value, then a summary naming the count, the healed file, and
/// where the previous file is preserved. Silent when nothing was healed — a
/// complete config's boot log is unchanged.
fn log_outcome(outcome: &HealOutcome, env_vars: &[(String, String)]) {
    if outcome.inserted.is_empty() {
        return;
    }
    let config = outcome
        .config_path
        .as_deref()
        .map_or_else(String::new, |path| path.display().to_string());
    for field in &outcome.inserted {
        info!(
            field = field.path,
            value = field.value,
            config = %config,
            "config heal inserted a missing required field with its declared default"
        );
        let Some(entry) = BOOT_REQUIRED_FIELD_DEFAULTS
            .iter()
            .find(|entry| entry.path == field.path)
        else {
            continue;
        };
        if let Some((variable, env_value)) = env_vars
            .iter()
            .find(|(name, _)| name.as_str() == entry.env_override)
        {
            warn!(
                field = field.path,
                override_variable = %variable,
                file_value = field.value,
                effective_runtime_value = %env_value,
                "config heal minted a field whose environment override rules at \
                 runtime: the file now carries the declared default, but this \
                 server runs the override's value"
            );
        }
    }
    let backup = outcome
        .backup_path
        .as_deref()
        .map_or_else(String::new, |path| path.display().to_string());
    info!(
        config = %config,
        backup = %backup,
        inserted_field_count = outcome.inserted.len(),
        "config heal wrote the healed config; the previous file is preserved beside it"
    );
}

#[cfg(test)]
#[path = "heal_tests.rs"]
mod tests;