Skip to content

fix(userconfig): harden global config file management#3529

Merged
Sayt-0 merged 1 commit into
mainfrom
fix/userconfig-global-config-hardening
Jul 8, 2026
Merged

fix(userconfig): harden global config file management#3529
Sayt-0 merged 1 commit into
mainfrom
fix/userconfig-global-config-hardening

Conversation

@Sayt-0

@Sayt-0 Sayt-0 commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

A check-up of the global config file management (~/.config/cagent/config.yaml)
surfaced one crash and several silent data-loss paths in the load/save cycle.
Every issue found is fixed here.

Issues and fixes

# Issue Fix
1 TUI panics at startup when the config file is malformed: Get() returns Settings{RestoreTabs: nil} on load error and tabs.go dereferences it unconditionally Nil-safe GetRestoreTabs() getter (consistent with GetSound() etc.); Get() error path returns normalized defaults
2 A YAML typo silently reverts all settings to defaults and disables aliases, with no message anywhere Load errors are logged in Get(), ResolveAlias, resolve(), and the credential-helper source; docker agent doctor gains a "User configuration" section and reports an unparseable config as an issue (non-zero exit)
3 Any TUI toggle (theme, layout, split-diff) rewrites the whole file, destroying hand-written comments and keys from newer versions Comments round-trip via goccy CommentMap; unknown keys are preserved through ,inline maps on Config and Settings (marshal falls back to plain serialization if comment re-attachment fails, so a save never fails because of a comment)
4 Concurrent writers overwrite each other (last writer wins): e.g. docker agent sandbox allow while the board is open is wiped by the board's next save New userconfig.Update(mutate) serializes load-mutate-save behind an advisory file lock (flock on unix, LockFileEx on windows, 5s timeout, best effort); all writers migrated: alias add/rm, sandbox allow/deny, theme, layout, split-diff, board
5 settings.hooks is consumed without validation, unlike hook drop-ins GlobalHooks() validates and skips invalid hooks with a warning; the section stays untouched on disk
6 version: is stamped on save but never checked on load Unknown versions are logged and loaded as v1
7 Save() marshals without the mutex that guards Aliases saveTo and GetSettings take the config mutex

Write path after the change

writer (CLI command, TUI toggle, board)
  └─ userconfig.Update(mutate)
       ├─ flock config.yaml.lock        (cross-process serialization)
       ├─ Load()                        (freshest on-disk state + comments)
       ├─ mutate(cfg)                   (error aborts, file untouched)
       └─ Save()                        (atomic write, comments and unknown keys preserved)

The board now refreshes its cached config from disk on every save instead of
rewriting a snapshot held since startup, so changes made by other processes to
non-board sections survive a board session.

Behavior notes

  • sandbox deny of an absent host no longer rewrites the config file.
  • Read-only paths (Get, alias resolution, completion) take no lock: the
    atomic write already guarantees a consistent view.
  • If the lock cannot be acquired within 5s, the update proceeds unlocked and
    logs a warning, matching the previous behavior rather than failing the save.

Tests

  • Malformed config: Get() returns safe defaults, no panic
  • Comments, unknown keys (both levels), and hooks survive a load/save cycle
  • Clearing a commented section still saves
  • 8 concurrent Update writers lose no changes (also run with -race)
  • Mutate error leaves the file byte-identical
  • Invalid global hooks never reach the runtime; valid ones pass through
  • Unknown config version still loads
  • doctor reports an unparseable user config and exits non-zero

Pre-existing SSRF/network test failures (TestURLSource_Read_RejectsLocalAddresses,
TestNewSSRFSafeTransport_RefusesPrivateIP, ...) are unrelated: they fail
identically on a clean tree in this environment because a local proxy
intercepts the requests those tests expect to be refused.

- fix TUI startup panic when the config file is malformed
  (unchecked *RestoreTabs deref); add nil-safe GetRestoreTabs
- log ignored config load errors instead of silently falling back
  to defaults, and surface an unparseable config in `doctor`
- preserve YAML comments and unknown keys across load/save cycles
  so hand-edits and newer-version fields survive TUI toggles
- add userconfig.Update: file-locked load-mutate-save cycle, and
  migrate all writers (alias, sandbox, theme, layout, board) to it
  so concurrent processes no longer overwrite each other
- validate user-config global hooks like hook drop-ins
- warn on unknown config version; marshal under the config mutex
@Sayt-0 Sayt-0 requested a review from a team as a code owner July 8, 2026 09:56
@aheritier aheritier added area/cli CLI commands, flags, output formatting area/config For configuration parsing, YAML, environment variables area/docs Documentation changes area/tui For features/issues/fixes related to the TUI kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Jul 8, 2026
@Sayt-0 Sayt-0 merged commit 5457b3f into main Jul 8, 2026
18 checks passed
@Sayt-0 Sayt-0 deleted the fix/userconfig-global-config-hardening branch July 8, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli CLI commands, flags, output formatting area/config For configuration parsing, YAML, environment variables area/docs Documentation changes area/tui For features/issues/fixes related to the TUI kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants