Skip to content

F1.A — config.json: schema, atomic write, fs.watch hot reload #24

@HXYerror

Description

@HXYerror

Part of #23.

Background

No persistent config file exists today. State lives in src/lib/state.ts (global mutable singleton) and CLI flags via citty in src/main.ts. We need a versioned on-disk config to hold model aliases, retention knobs, and feature toggles for the admin plane.

Goal

Typed config store at ~/.local/share/copilot-api/config.json with atomic writes and fs.watch-based hot reload, used as the source of truth by F1.B / F1.C / F2 / F3 / F4.

Tasks

  • Define zod schema:
    • version: 1
    • models: Record<alias, { upstream: string; enabled: boolean; allowed_keys: string[] }> (use ["*"] for global allow)
    • retention: { events_days: number; traces_days: number; traces_max_bytes: number }
    • features: { auth: boolean; telemetry: boolean; debug: boolean }
  • loadConfig() / saveConfig() with atomic write: write *.tmp, fsync file, rename, fsync parent directory (crash-consistency on ext4/xfs)
  • Set file mode 0600 on every write; verify on load and warn loudly if wider (security hardening)
  • Hot reload: watch the directory (not the file — editors do atomic rename, fs.watch loses inode on macOS); 250 ms debounce; on parse error keep previous value, log warning, do not crash
  • Snapshot semantics: expose config.snapshot() returning a frozen copy. In-flight requests MUST capture a snapshot at request start so a mid-request reload doesn't break SSE alias rewriting (per backend review Preserve encrypted_content for multi-turn reasoning #6).
  • Migration runner stub: read version, transform if needed, rewrite atomically
  • Default config seeded on first run if absent
  • Tests: schema validation, atomic write under stress (no partial files), fs.watch debounce, atomic-rename detection (macOS pattern), snapshot isolation from concurrent reload

Acceptance criteria

  • Editing the file via vi config.json (uses atomic rename) propagates to in-memory aliases within 1 s
  • Concurrent saves never produce a partial-write file (verified via stress test)
  • A malformed write keeps the process alive on the previous valid config
  • Snapshot held by an in-flight handler is unchanged by a concurrent reload
  • File mode is 0600 after every write

File pointers

  • New: src/lib/config-store.ts, tests/config-store.test.ts
  • Touch: src/lib/paths.ts (add configPath()), src/lib/state.ts (read from store)

Dependencies

Blocks F1.B, F1.C, F2.B, F3.A, F4.A (everyone reads from this).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions