Skip to content

fix(settings): trust-split save so /settings changes survive the P0-1 allowlist#105

Merged
shoom1 merged 1 commit into
developfrom
fix/settings-save-trust-split
Jul 16, 2026
Merged

fix(settings): trust-split save so /settings changes survive the P0-1 allowlist#105
shoom1 merged 1 commit into
developfrom
fix/settings-save-trust-split

Conversation

@shoom1

@shoom1 shoom1 commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Problem

P0-1 (#103) added a deny-by-default allowlist on the load side: _AllowlistFilterSource drops non-allowlisted keys from the project ./.{app}/settings.json. But SettingsPersistence.save() (used by /settings) still dumped every field to the project file, so writer and reader disagreed:

  • every /settings save recreated the untrusted_project_setting_ignored warning storm on the next launch (observed in research_demo: 23 warnings from a stale kitchen-sink file);
  • worse, a /settings change to a user-scoped key (e.g. stateful_executor_backend, raw_llm_logging) was written to the project file and then silently reverted on the next start.

Fix

save() now mirrors the load-side trust model:

  • Project file receives only PROJECT_SETTABLE_KEYS, fully rewritten — which also heals stale pre-P0-1 kitchen-sink files.
  • User ~/.{app}/settings.json receives user-scoped keys that differ from the settings class default, via read-merge-write: unmanaged keys (hand-stored API keys, domain keys) are preserved verbatim; a malformed user file raises instead of being clobbered; reverting a key to its default removes it, so reverts stick and future code-default changes keep applying to untouched fields.
  • Explicit save(path=...) keeps the legacy single-file full dump.

PROJECT_SETTABLE_KEYS moves to settings_persistence.py (config.py imports it; the reverse import would be circular), so the filter and the split cannot drift apart. save() returns a SettingsSaveResult and /settings reports both destinations.

Tests

TDD: 8 new tests in TestTrustSplitSave written first and watched fail — including the round-trip regression (stateful_executor_backend change must survive save + fresh load with zero untrusted-key warnings), healing of a stale project file, merge preservation of hand-stored secrets, and revert removal. Full offline suite: 1977 passed (-m 'not llm and not docker').

https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv

… allowlist

P0-1 (#103) made the loader drop non-allowlisted keys from the project
settings.json, but save() still dumped every field there: each /settings
save recreated the untrusted_project_setting_ignored warning storm, and a
change to a user-scoped key (e.g. stateful_executor_backend) was silently
reverted on the next start.

save() now mirrors the load-side trust model:
- project file gets ONLY allowlisted keys, fully rewritten (heals stale
  pre-P0-1 kitchen-sink files)
- user ~/.{app}/settings.json gets user-scoped keys that differ from the
  settings class default, via read-merge-write that preserves unmanaged
  keys (hand-stored secrets, domain keys); reverting a key to its default
  removes it so the revert sticks and code-default changes keep applying
- explicit save(path=...) keeps the legacy single-file full dump

PROJECT_SETTABLE_KEYS moves to settings_persistence (config.py imports it;
the reverse would be circular) so writer and reader share one allowlist.
save() returns SettingsSaveResult; /settings reports both paths.

Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
@shoom1
shoom1 merged commit ba86781 into develop Jul 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant