Skip to content

Establish workspace/config/ and multi-source config resolution (3/13) - #206

Open
alex-clickhouse wants to merge 1 commit into
config-refactor/02-env-varsfrom
config-refactor/03-config-subdir
Open

Establish workspace/config/ and multi-source config resolution (3/13)#206
alex-clickhouse wants to merge 1 commit into
config-refactor/02-env-varsfrom
config-refactor/03-config-subdir

Conversation

@alex-clickhouse

@alex-clickhouse alex-clickhouse commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a third config layer: <workspace>/config/settings.yaml, git-tracked and merged beneath config.yaml and config.local.yaml, so machine-local values still override it. With no such subtree present, resolution is byte-for-byte what it was.

nerve init no longer writes everything to config.yaml. Its answers are split by whether the value would be wrong on another machine:

Layer Gets
config.yaml workspace, deployment, provider.aws_profile, gateway.ssl.*, proxy, docker, telegram.enabled, sync.gmail.accounts, external_agents, mcp_endpoint
settings.yaml timezone, gateway.host/port, provider.type/aws_region (with the region-scoped Bedrock model IDs), agent.*, memory.*, sessions.*, sync.*, quiet hours, telegram.dm_policy/stream_mode

A key goes in exactly one layer. Re-running nerve init rewrites config.yaml wholesale but merges settings.yaml, preserving keys the wizard does not own — a team policy setting survives. Where a re-run stops emitting a key it previously wrote, the key is deleted rather than omitted, since the merge would otherwise leave it at its old value forever.

Also fixes nerve backup, which enumerates workspace directories explicitly and so stopped capturing the tracked config once it moved: bundles kept every skill and identity file and no settings.

Why

The layer is the foundation for sharing config between machines, but adding it is not enough on its own, because config.yaml shadows it. If the wizard kept writing every key to config.yaml, settings.yaml would be dead on arrival — you could edit it, commit it, sync it to a second machine, and nothing would change, because the local file still wins. The split is what makes the new layer load-bearing rather than decorative.

The practical test for where a key belongs is lockdown, which drops config.yaml and config.local.yaml entirely. Anything left in the machine layer then falls back to its declared default on every locked box. So a key belongs there only when that default is an acceptable answer — which is why gateway.host/port and provider.type/aws_region are shared rather than local. Both were previously machine-local, and both are the deployment's shape rather than a per-machine fact:

  • The wizard only ever wrote 0.0.0.0 and 8900, identical to the declared defaults, so the tracked layer could not state a bind address at all and a fleet had no way to agree on a non-default port.
  • A locked instance whose provider block lived only in config.yaml lost it, silently reverted to the anthropic default, and then failed for a missing API key it would not otherwise have needed. The geo-scoped Bedrock model IDs (us./eu./apac.) are a function of the region alone, so once the region is tracked they can be tracked with it.

provider.aws_profile stays machine-local — it names an entry in one box's AWS credentials file — as do gateway.ssl.cert/key, which are local filesystem paths. Sharing a certificate path would stop the gateway binding on any machine that does not have that file.

external_agents also stays local for a different reason: the gateway rewrites that block at runtime on every enable/disable, and it must not be writing into a git-tracked file.

🤖 Generated with Claude Code

@alex-clickhouse alex-clickhouse changed the title Establish workspace/config/ and multi-source config resolution Establish workspace/config/ and multi-source config resolution (3/13) Jul 28, 2026
@alex-clickhouse
alex-clickhouse requested a review from Copilot July 28, 2026 10:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new git-tracked workspace config layer (<workspace>/config/settings.yaml) beneath the existing machine-local config.yaml and config.local.yaml, and updates init/backup/reload paths so the new layered config is created, loaded, validated, and included in backups without breaking legacy installs.

Changes:

  • Implement multi-layer config loading: workspace/config/settings.yaml < config.yaml < config.local.yaml, with env interpolation after merge.
  • Update nerve init scaffolding/writing to populate the workspace settings layer and keep re-init behavior safe via backups and ownership rules.
  • Ensure backups and MCP reload paths include/read the tracked workspace config subtree.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_config_sources.py New tests covering multi-layer merge semantics, strictness of workspace settings, env refs, and repairability via CLI commands.
tests/test_bootstrap.py Adjusts bootstrap expectations for the split between machine-local config and workspace-tracked settings; adds prompt wording test.
tests/test_backup.py Adds regression test ensuring backups include workspace/config/** (settings + cron).
nerve/workspace.py Adds scaffold installer for workspace/config/ from packaged templates.
nerve/templates/config/settings.yaml Introduces default commented scaffold for shareable workspace settings.
nerve/gateway/routes/mcp_servers.py Returns clean 400s on config errors during MCP reload instead of 500s.
nerve/config.py Adds YAML mapping reader with strict mode and implements multi-source config resolution + shared helpers.
nerve/cli.py Allows doctor/init to run even when config load fails; updates doctor to validate merged multi-layer view.
nerve/bootstrap.py Updates setup wizard to create/write the workspace settings layer and manage backups + re-init semantics.
nerve/backup.py Includes workspace/config in backup allowlist so tracked settings/cron survive restores.
nerve/agent/engine.py Reload MCP config from the daemon’s config dir (not process CWD) so reload matches startup behavior.
docs/config.md Documents the new 3-layer precedence model, init split/ownership, and env refs across all layers.
Comments suppressed due to low confidence (1)

nerve/bootstrap.py:2199

  • Write workspace/config/settings.yaml using an explicit UTF-8 encoding. This file is intended to safely contain non-ASCII content (prompt hints, names, etc.); relying on the process locale for encoding can cause UnicodeEncodeError under LC_ALL=C and similar environments.
        with open(settings_path, "w") as f:

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nerve/bootstrap.py Outdated
Comment thread nerve/bootstrap.py
Comment thread tests/test_config_sources.py
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/03-config-subdir branch from 727d3b6 to a6022f5 Compare July 28, 2026 12:11
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/03-config-subdir branch 2 times, most recently from d4ed397 to eeeb051 Compare July 28, 2026 13:14
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/03-config-subdir branch from eeeb051 to 99d0346 Compare July 28, 2026 14:23
@alex-clickhouse
alex-clickhouse requested a review from Copilot July 28, 2026 14:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

nerve/bootstrap.py:2666

  • _expand_workspace() claims to expand paths the same way the config loader does, but it currently doesn’t strip whitespace and it expands user/home before env vars. nerve.config._expand_path() strips first, then expandvars→expanduser, and strips again. With leading/trailing spaces (or values like " ${WS}"), the wizard can write settings.yaml into a different directory than the loader will later read, silently losing portable settings.
def _expand_workspace(raw: str) -> Path:
    """Expand a workspace path the way nerve.config resolves it."""
    if "${" in raw:
        raw = _interpolate_str(raw, [])
    return Path(os.path.expandvars(os.path.expanduser(raw)))

@alex-clickhouse
alex-clickhouse marked this pull request as ready for review July 28, 2026 14:44
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/03-config-subdir branch from 99d0346 to 68ef88a Compare July 28, 2026 14:54
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/03-config-subdir branch from 68ef88a to b1e1ea5 Compare July 28, 2026 15:33
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/03-config-subdir branch from b1e1ea5 to 0eb465c Compare July 29, 2026 10:31
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/03-config-subdir branch from 0eb465c to 3936790 Compare July 30, 2026 08:01
Shareable configuration moves into <workspace>/config/settings.yaml, a
git-tracked subtree, and is merged beneath the two machine-local layers:
settings.yaml < config.yaml < config.local.yaml. An absent subtree keeps the
previous behaviour.

A settings.yaml that is not a mapping now fails loudly instead of being merged
as junk, a file that exists but cannot be read is reported as such rather than
read as empty, and the YAML encoding is pinned so a mis-encoded file cannot
decode differently on another machine. nerve doctor stays alive on a bad config
so it can report it, and nerve backup captures the new subtree.

Co-Authored-By: Claude <noreply@anthropic.com>
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/03-config-subdir branch from 3936790 to 94e01b4 Compare July 30, 2026 10:06
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.

2 participants