fix(config): load config_secrets.json on hosts without os.geteuid (#590)

ensure_shared_group_ownership() - the chgrp self-heal ConfigManager runs
before reading config_secrets.json (#416) - looked up os.geteuid
unguarded. That name does not exist on Windows, and the AttributeError
is not an OSError, so it escaped the helper's best-effort handling and
every except clause in load_config(). Any Windows checkout with a
config/config_secrets.json got a ConfigError from every config load and
could not import web_interface.app.

That is what made test_update_all_plugins.py error at setup: its client
fixture imports web_interface.app. It was not state leaked between test
files - the trigger is whether the checkout has a secrets file.

Return early when os.geteuid or os.chown is missing. No change on POSIX.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-16 17:12:18 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent f9b3d6ae52
commit 2082665252
4 changed files with 105 additions and 6 deletions
+24
View File
@@ -333,3 +333,27 @@ class TestArraySecretStripAndMerge:
manager._deep_merge(stripped, secrets)
assert stripped == full # round trip restores the items
class TestLoadWithoutPosixOwnershipApis:
"""A secrets file must load on platforms that have no uid/gid at all.
load_config() chgrp's the secrets file to the shared group before reading
it, to self-heal a root-written file the non-root web user can't read.
That helper is documented as best-effort, but it looked up os.geteuid
unguarded -- absent on Windows -- and the resulting AttributeError is not
an OSError, so it escaped every except clause on the way out. The symptom
was a ConfigError from load_config on any Windows checkout carrying a
config/config_secrets.json, which took `import web_interface.app` with it.
"""
def test_secrets_still_merge_without_geteuid(self, tmp_path, monkeypatch):
monkeypatch.delattr(os, "geteuid", raising=False)
monkeypatch.delattr(os, "chown", raising=False)
manager = make_manager(
tmp_path,
config={"weather": {"city": "Austin"}},
secrets={"weather": {"api_key": "s3cret"}},
)
assert manager.load_config()["weather"] == {"city": "Austin", "api_key": "s3cret"}