Commit graph

1 commit

Author SHA1 Message Date
3351e7b444 fix(v0.6-tail-4): register !include YAML tag constructor in config loader -- closes prod PUT 500
Pre-existing issue surfaced by v0.6-tail-3: prod config at
/data/config/config.yaml:82 uses !include to compose from separate
files, but the loader had no constructor registered so PUT
/api/config/<section> returned 500 with could-not-determine-constructor
when the section save path round-tripped YAML. This adds the !include
constructor (read path) + preserves the include structure on write so
multi-file config layouts work end-to-end via the GUI. The runtime
config behavior is unchanged; this only fixes the PUT-and-round-trip
case.

Implementation note: the read-only runtime path
(_load_yaml_with_includes) already had a working !include constructor
that recursively substitutes content. The bug was specifically in
save_section() -- it used plain yaml.safe_load() to re-read the target
file off disk for secret-ref preservation and for in-place section
updates. When target_file == "config.yaml" that file contains !include
directives for OTHER sections, and safe_load died on them.

Adding a third constructor that substitutes !include on save would
have flattened the multi-file layout to a single file the first time
anyone PUT an inline section. Instead this commit adds a preserve-mode
loader/dumper pair:

  - _load_yaml_preserve() returns an Include("path") sentinel for each
    !include node instead of recursing into the referenced file.
  - _dump_yaml_preserve() re-emits Include("path") back to disk as
    `!include path`. (PyYAML auto-quotes when the scalar contains a
    period, so the on-disk form is `!include 'foo.yaml'`; both forms
    are equivalent at parse time.)
  - save_section()'s three yaml-touching sites (the secret-ref raw
    read, the existing-target read, and the final dump) now use these
    helpers. Local.yaml stays on yaml.safe_load/dump because local.yaml
    never contains !include.

The runtime loader is untouched, so boot-time config still substitutes
includes and Config dataclasses see real values. Only the GUI's
section-save path round-trips through the preserve helpers.

Tests (tests/test_include_roundtrip.py, 8 cases):
- Runtime loader still substitutes !include content (regression guard)
- Preserve loader returns Include() sentinels
- Preserve dumper re-emits `!include path` (tolerant of PyYAML
  auto-quoting)
- Read -> write -> read identity through the preserve helpers
- save_section('bot', ...) on a config.yaml that uses !include for
  sibling sections succeeds AND leaves the includes intact on disk
  (this is the exact prod PUT 500 case from v0.6-tail-3)
- After save_section, the runtime loader re-resolves all !include
  files AND sees the saved change to the inline section
- save_section on a dedicated file (env_feeds.yaml) writes only that
  file; config.yaml's !include directives are untouched
- Runtime cycle detection still trips on A!include->B!include->A

Live verification on CT108 after rebuild:
  PUT /api/config/bot {"name":"AIDA","owner":"Malice","respond_to_dms":true,"filter_bbs_protocols":true}
  -> HTTP 200 {"saved":true,"restart_required":false,"changed_keys":[]}
  /data/config/config.yaml retains all 7 !include directives
  (meshtastic, mesh_sources, mesh_intelligence, environmental,
  notifications, llm, dashboard)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 04:37:24 +00:00