From 53f2d9db3faeabf4a2a6fd44861b1ff812847d0b Mon Sep 17 00:00:00 2001 From: Bill Wang Date: Fri, 23 Oct 2026 04:10:19 +0000 Subject: [PATCH 1/2] Add an opt-in to trust OS-managed settings Co-authored-by: Isaac --- README.md | 6 ++++++ src/ucode/managed_files.py | 15 +++++++++++---- tests/README.md | 5 +++++ tests/integration/README.md | 5 +++++ tests/test_agent_claude.py | 14 ++++++++++++-- tests/test_agent_codex.py | 13 +++++++++++-- tests/test_managed_files.py | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index db55532e5..ab713c213 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,12 @@ precedence; later launches use whatever files are present. The choice is saved for launches after a successful configure; `ug configure` without `--user` and `ug revert` return to `--global`. +For hosts whose OS-managed settings are maintained externally, launch with +`UG_TRUST_OS_SETTINGS=1 ug claude` (or `ug codex`). This switch alone skips UG's +OS-managed writes and stale-settings conflict check; UG still generates per-user settings. +The agent combines its settings normally, including the higher precedence of OS-managed settings. +`UG_SKIP_OS_SETTINGS=1` continues to check for conflicting managed settings. + Available coding agents are `codex`, `claude`, `gemini`, `opencode`, `copilot`, and `pi`. `cursor` can be included in `--agents` for MCP-only setup; Cursor models still run through your Cursor account. diff --git a/src/ucode/managed_files.py b/src/ucode/managed_files.py index 6e6d780e2..ea1604ee6 100644 --- a/src/ucode/managed_files.py +++ b/src/ucode/managed_files.py @@ -34,8 +34,10 @@ # Absolute path so a stripped PATH (desktop/GUI launchers) still finds it. _SUDO = "/usr/bin/sudo" -# Opt out of all OS-managed settings work (read, conflict check, and the privileged sudo write). +# Opt out of OS-managed writes while retaining the stale-settings conflict check. SKIP_OS_SETTINGS_ENV = "UG_SKIP_OS_SETTINGS" +# Trust native agent precedence without checking or modifying OS-managed settings. +TRUST_OS_SETTINGS_ENV = "UG_TRUST_OS_SETTINGS" CONFIG_SCOPES = ("global", "user") DEFAULT_CONFIG_SCOPE = "global" _config_scope = DEFAULT_CONFIG_SCOPE @@ -107,7 +109,7 @@ def config_scope() -> str: def managed_settings_skipped() -> bool: - """Whether OS-managed settings are opted out via ``UG_SKIP_OS_SETTINGS`` or the ``user`` scope. + """Whether OS-managed writes are opted out via environment or the ``user`` scope. Set the variable to ``1``/``true``/``yes``/``on`` to make ucode neither read nor update the root-owned managed files (Claude Code ``managed-settings.json``, Codex ``managed_config.toml`` @@ -117,8 +119,12 @@ def managed_settings_skipped() -> bool: Skips the privileged ``sudo`` step entirely, so an interactive run is never prompted for a password and never hits the "OS-managed settings could not be updated" failure on hosts where that write is blocked. ``ug configure --user`` has the same effect. + ``UG_TRUST_OS_SETTINGS`` additionally bypasses the stale-settings conflict check, leaving + native agents to combine the existing managed settings with the per-user settings. """ - return env_bool(SKIP_OS_SETTINGS_ENV) or _config_scope == "user" + return ( + env_bool(SKIP_OS_SETTINGS_ENV) or env_bool(TRUST_OS_SETTINGS_ENV) or _config_scope == "user" + ) def managed_files_supported() -> bool: @@ -462,8 +468,9 @@ def check_skipped_managed_overrides( workspace keeps its highest-precedence gateway values, so refuse to proceed when the conflicting keys would let a bare agent silently reach the old workspace. Reads only (no sudo); best-effort, so an unsupported platform, symlink, or unparseable file is left to the normal reconcile path. + ``UG_TRUST_OS_SETTINGS`` skips this check and leaves precedence to the native agent. """ - if path is None or path.is_symlink(): + if env_bool(TRUST_OS_SETTINGS_ENV) or path is None or path.is_symlink(): return current_text = read_managed_file(path) if current_text is None: diff --git a/tests/README.md b/tests/README.md index 01d4f604e..1885a7585 100644 --- a/tests/README.md +++ b/tests/README.md @@ -357,6 +357,11 @@ authorization, and one-shot `sudo -n` with disconnected stdin. These are compone privileged writes replaced by temporary-file operations, not automated Isaac, real sudo-policy, or live telemetry-export coverage. +`UG_TRUST_OS_SETTINGS` component cases in those three files cover standalone and combined +opt-outs, boolean parsing, user-config generation despite conflicting managed values, and no +managed write or conflict read. Existing `UG_SKIP_OS_SETTINGS` rejection remains covered. +Native settings precedence and laptop-hosted Omnigent sessions are not live-tested here. + Managed smart defaults are covered by `test_managed_config.py`, `test_cli.py`, `test_managed_setup.py`, `test_databricks.py`, and `test_managed_budget.py`: parsing the `smart_defaults` wire field, reading older `spend_tiers` caches, skipping recommendations diff --git a/tests/integration/README.md b/tests/integration/README.md index 9d9dde8e9..99b5f0395 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -243,6 +243,11 @@ handling without sudo. This integration suite does not yet assert password-prompt counts with sudo credential caching disabled; that requires a disposable workstation/VM with an explicit sudo policy. +`UG_TRUST_OS_SETTINGS` is covered by component tests in `../test_managed_files.py`, +`../test_agent_claude.py`, and `../test_agent_codex.py`: no managed writes/checks and +continued per-user config generation. This suite does not cover the switch with live +agent settings precedence or laptop-hosted Omnigent sessions. + Caller permission-deny preservation is covered by `TestBuildClaudeArgv` in `../test_agent_claude.py`, including direct, relayed, and routing configuration output. Native parent/child enforcement and the distinction between technical search exclusion diff --git a/tests/test_agent_claude.py b/tests/test_agent_claude.py index 23476d883..4f717c3c8 100644 --- a/tests/test_agent_claude.py +++ b/tests/test_agent_claude.py @@ -1137,7 +1137,8 @@ def test_skip_env_errors_on_stale_managed_override(self, monkeypatch): assert "stale" in message assert "ug revert" in message - def test_user_scope_launches_over_stale_managed_override(self, monkeypatch): + @pytest.mark.parametrize("opt_out", ["user_scope", "trust", "trust_and_skip"]) + def test_opt_out_launches_over_stale_managed_override(self, monkeypatch, opt_out): private_writes: list = [] managed_writes: list = [] existing = { @@ -1148,10 +1149,19 @@ def test_user_scope_launches_over_stale_managed_override(self, monkeypatch): } } self._patch(monkeypatch, private_writes, managed_writes, existing) - managed_files.set_config_scope("user") + if opt_out == "user_scope": + managed_files.set_config_scope("user") + else: + monkeypatch.setenv(managed_files.TRUST_OS_SETTINGS_ENV, "1") + if opt_out == "trust_and_skip": + monkeypatch.setenv(managed_files.SKIP_OS_SETTINGS_ENV, "1") + monkeypatch.setattr( + claude, "reconcile_managed_file", lambda *a, **kw: pytest.fail("must not write managed") + ) state = {"workspace": WS, "codex_models": []} claude.write_tool_config(state, "databricks-claude-sonnet-4") assert managed_writes == [] + assert str(claude.CLAUDE_SETTINGS_PATH) in [p for p, _ in private_writes] def test_skip_env_no_error_without_stale_override(self, monkeypatch): private_writes: list = [] diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index b76a4dd6b..e7439d32f 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -1797,7 +1797,8 @@ def test_skip_env_errors_on_stale_managed_override(self, tmp_path, monkeypatch): assert "stale" in message assert "ug revert" in message - def test_user_scope_launches_over_stale_managed_override(self, tmp_path, monkeypatch): + @pytest.mark.parametrize("opt_out", ["user_scope", "trust", "trust_and_skip"]) + def test_opt_out_launches_over_stale_managed_override(self, tmp_path, monkeypatch, opt_out): config_path, managed_path = self._patch(tmp_path, monkeypatch) managed_path.parent.mkdir(parents=True, exist_ok=True) managed_path.write_text( @@ -1806,7 +1807,15 @@ def test_user_scope_launches_over_stale_managed_override(self, tmp_path, monkeyp 'base_url = "https://old-workspace.databricks.com/ai-gateway/openai/v1"\n', encoding="utf-8", ) - managed_files.set_config_scope("user") + if opt_out == "user_scope": + managed_files.set_config_scope("user") + else: + monkeypatch.setenv(managed_files.TRUST_OS_SETTINGS_ENV, "1") + if opt_out == "trust_and_skip": + monkeypatch.setenv(managed_files.SKIP_OS_SETTINGS_ENV, "1") + monkeypatch.setattr( + codex, "reconcile_managed_file", lambda *a, **kw: pytest.fail("must not write managed") + ) codex.write_tool_config({"workspace": WS, "codex_models": ["gpt-5"]}) diff --git a/tests/test_managed_files.py b/tests/test_managed_files.py index 8c36c53e4..703a327a8 100644 --- a/tests/test_managed_files.py +++ b/tests/test_managed_files.py @@ -724,6 +724,39 @@ def test_skip_prevents_privileged_write(self, monkeypatch): ) assert result == "unsupported" + @pytest.mark.parametrize("value", ["1", "true", "TRUE", "Yes", "on", " on "]) + def test_trust_skips_writes_and_conflict_reads(self, monkeypatch, value): + monkeypatch.delenv(managed_files.SKIP_OS_SETTINGS_ENV, raising=False) + monkeypatch.setenv(managed_files.TRUST_OS_SETTINGS_ENV, value) + assert managed_files.managed_settings_skipped() is True + assert _REAL_MANAGED_FILES_SUPPORTED() is False + monkeypatch.setattr( + managed_files, "read_managed_file", lambda *a: pytest.fail("must not check managed") + ) + managed_files.check_skipped_managed_overrides( + Path("/etc/claude-code/managed-settings.json"), + parser=json.loads, + compose=lambda settings: settings, + owned_paths=[["env", "ANTHROPIC_BASE_URL"]], + display="Claude Code", + ) + + @pytest.mark.parametrize("value", ["0", "false", "off", "no", ""]) + def test_falsey_trust_keeps_conflict_check(self, tmp_path, monkeypatch, value): + monkeypatch.delenv(managed_files.SKIP_OS_SETTINGS_ENV, raising=False) + monkeypatch.setenv(managed_files.TRUST_OS_SETTINGS_ENV, value) + assert managed_files.managed_settings_skipped() is False + path = tmp_path / "managed-settings.json" + path.write_text('{"apiKeyHelper": "old-helper"}', encoding="utf-8") + with pytest.raises(RuntimeError, match="stale and conflict"): + managed_files.check_skipped_managed_overrides( + path, + parser=json.loads, + compose=lambda settings: {"apiKeyHelper": "new-helper"}, + owned_paths=[["apiKeyHelper"]], + display="Claude Code", + ) + def test_raise_on_skipped_managed_overrides_reports_stale_leaves(self): with pytest.raises(RuntimeError) as excinfo: managed_files.raise_on_skipped_managed_overrides( From aeea10aebb3e53d5f8701fa4fc8dbca0f4cc22e7 Mon Sep 17 00:00:00 2001 From: Bill Wang Date: Fri, 23 Oct 2026 21:31:19 +0000 Subject: [PATCH 2/2] Trust OS-managed settings under UG_SKIP_OS_SETTINGS Drop the UG_TRUST_OS_SETTINGS variable. UG_SKIP_OS_SETTINGS now also skips the stale-settings check: ug neither writes nor checks the OS-managed files and proceeds as usual, leaving precedence to the agents. This matches what `ug configure --user` launches already did. Remove the now-unused check_skipped_managed_overrides and its error helper. Co-authored-by: Isaac --- README.md | 9 +--- docs/os-managed-settings-design.md | 6 +-- src/ucode/agents/claude.py | 11 +---- src/ucode/agents/codex.py | 11 +---- src/ucode/managed_files.py | 66 +++--------------------------- tests/README.md | 5 --- tests/integration/README.md | 5 --- tests/test_agent_claude.py | 27 +++--------- tests/test_agent_codex.py | 35 +++++----------- tests/test_managed_files.py | 64 ----------------------------- 10 files changed, 29 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index ab713c213..96faf166a 100644 --- a/README.md +++ b/README.md @@ -127,17 +127,12 @@ ug configure --user `--global` (the default) writes the machine-wide Claude Code and Codex managed settings with sudo. `--user` skips them, like `UG_SKIP_OS_SETTINGS`, and needs no sudo; use it when you launch through -`ug`. Configure fails while a managed file for the agent exists, since managed settings take +`ug`. `UG_SKIP_OS_SETTINGS=1` neither writes nor checks the OS-managed files: ug trusts whatever is +there and the agents apply their native precedence. Configure fails while a managed file for the agent exists, since managed settings take precedence; later launches use whatever files are present. The choice is saved for launches after a successful configure; `ug configure` without `--user` and `ug revert` return to `--global`. -For hosts whose OS-managed settings are maintained externally, launch with -`UG_TRUST_OS_SETTINGS=1 ug claude` (or `ug codex`). This switch alone skips UG's -OS-managed writes and stale-settings conflict check; UG still generates per-user settings. -The agent combines its settings normally, including the higher precedence of OS-managed settings. -`UG_SKIP_OS_SETTINGS=1` continues to check for conflicting managed settings. - Available coding agents are `codex`, `claude`, `gemini`, `opencode`, `copilot`, and `pi`. `cursor` can be included in `--agents` for MCP-only setup; Cursor models still run through your Cursor account. diff --git a/docs/os-managed-settings-design.md b/docs/os-managed-settings-design.md index 463a7fd2d..245f0e434 100644 --- a/docs/os-managed-settings-design.md +++ b/docs/os-managed-settings-design.md @@ -34,9 +34,9 @@ OS-managed files like `UG_SKIP_OS_SETTINGS`: no administrator permission, and ma register in user scope. Because OS-managed settings take precedence over user settings, `ug configure --user` fails before configuring any agent while a selected agent's OS-managed file exists (including Windows `requirements.toml`), whoever wrote it; `ug revert`, removal, or -`--global` resolves it. Launches don't check: a managed file added later simply takes precedence, -without the `UG_SKIP_OS_SETTINGS` stale-override error. `ug configure` without `--user` and -`ug revert` reset the scope to `global`. +`--global` resolves it. Launches don't check: a managed file added later simply takes precedence. +`UG_SKIP_OS_SETTINGS` doesn't check either: ug trusts the OS-managed files present and leaves them +untouched. `ug configure` without `--user` and `ug revert` reset the scope to `global`. ## Interactive Detection diff --git a/src/ucode/agents/claude.py b/src/ucode/agents/claude.py index 250439294..47ab12ab1 100644 --- a/src/ucode/agents/claude.py +++ b/src/ucode/agents/claude.py @@ -58,8 +58,6 @@ ManagedFileWriteUnavailable, SettingsPassthrough, apply_settings_passthrough, - check_skipped_managed_overrides, - config_scope, current_os, managed_file_conflicts, managed_file_is_verified, @@ -1880,14 +1878,7 @@ def _reconcile_managed_settings( during `ucode claude`, so a bare `claude` could not reach the gateway anyway. """ if managed_settings_skipped(): - if config_scope() != "user": - check_skipped_managed_overrides( - _managed_settings_path(), - parser=_parse_managed_settings, - compose=compose, - owned_paths=owned_paths, - display="Claude Code", - ) + # Opted out: trust whatever OS-managed file exists and leave it untouched. return path = _managed_settings_path() if path is None: diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index 5c5794ef5..99ece5d21 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -68,8 +68,6 @@ CODEX_REQUIREMENTS_FILE, OS, ManagedFileWriteUnavailable, - check_skipped_managed_overrides, - config_scope, current_os, managed_file_conflicts, managed_file_is_verified, @@ -900,14 +898,7 @@ def _reconcile_managed_config(state: dict, compose: Callable[[dict], dict]) -> N if path is not None and path.name == CODEX_REQUIREMENTS_FILE: compose = _as_requirements(compose) if managed_settings_skipped(): - if config_scope() != "user": - check_skipped_managed_overrides( - path, - parser=_parse_managed_config, - compose=compose, - owned_paths=MANAGED_KEYS, - display="Codex", - ) + # Opted out: trust whatever OS-managed file exists and leave it untouched. return if path is None: print_warning_err( diff --git a/src/ucode/managed_files.py b/src/ucode/managed_files.py index ea1604ee6..91b4cce59 100644 --- a/src/ucode/managed_files.py +++ b/src/ucode/managed_files.py @@ -34,10 +34,8 @@ # Absolute path so a stripped PATH (desktop/GUI launchers) still finds it. _SUDO = "/usr/bin/sudo" -# Opt out of OS-managed writes while retaining the stale-settings conflict check. +# Opt out of all OS-managed settings work (read, conflict check, and the privileged sudo write). SKIP_OS_SETTINGS_ENV = "UG_SKIP_OS_SETTINGS" -# Trust native agent precedence without checking or modifying OS-managed settings. -TRUST_OS_SETTINGS_ENV = "UG_TRUST_OS_SETTINGS" CONFIG_SCOPES = ("global", "user") DEFAULT_CONFIG_SCOPE = "global" _config_scope = DEFAULT_CONFIG_SCOPE @@ -109,7 +107,7 @@ def config_scope() -> str: def managed_settings_skipped() -> bool: - """Whether OS-managed writes are opted out via environment or the ``user`` scope. + """Whether OS-managed settings are opted out via ``UG_SKIP_OS_SETTINGS`` or the ``user`` scope. Set the variable to ``1``/``true``/``yes``/``on`` to make ucode neither read nor update the root-owned managed files (Claude Code ``managed-settings.json``, Codex ``managed_config.toml`` @@ -118,13 +116,11 @@ def managed_settings_skipped() -> bool: always launches through ``ug`` and never needs a bare ``claude``/``codex`` to reach the gateway. Skips the privileged ``sudo`` step entirely, so an interactive run is never prompted for a password and never hits the "OS-managed settings could not be updated" failure on hosts where - that write is blocked. ``ug configure --user`` has the same effect. - ``UG_TRUST_OS_SETTINGS`` additionally bypasses the stale-settings conflict check, leaving - native agents to combine the existing managed settings with the per-user settings. + that write is blocked. ug also trusts whatever OS-managed files exist and doesn't check them + against its own settings; the agents apply their native precedence. ``ug configure --user`` has + the same effect. """ - return ( - env_bool(SKIP_OS_SETTINGS_ENV) or env_bool(TRUST_OS_SETTINGS_ENV) or _config_scope == "user" - ) + return env_bool(SKIP_OS_SETTINGS_ENV) or _config_scope == "user" def managed_files_supported() -> bool: @@ -438,56 +434,6 @@ def managed_file_conflicts( return conflicts -def raise_on_skipped_managed_overrides( - path: Path, - conflicts: list[str], - *, - display: str, -) -> None: - """Fail when an opt-out left a stale OS-managed file overriding the new user config.""" - if not conflicts: - return - raise RuntimeError( - f"the OS-managed {display} settings at {path} are stale and conflict with your per-user settings." - f" {SKIP_OS_SETTINGS_ENV} is set, so the OS-managed settings were not modified. Run `ug revert` " - f"from an interactive terminal or remove {path}" - ) - - -def check_skipped_managed_overrides( - path: Path | None, - *, - parser: ManagedParser, - compose: Callable[[dict], dict], - owned_paths: list[list[str]], - display: str, -) -> None: - """When opting out, fail if a stale OS-managed file still overrides the new user config. - - `UG_SKIP_OS_SETTINGS` leaves the root-owned managed file untouched. A file from an earlier - workspace keeps its highest-precedence gateway values, so refuse to proceed when the conflicting - keys would let a bare agent silently reach the old workspace. Reads only (no sudo); best-effort, - so an unsupported platform, symlink, or unparseable file is left to the normal reconcile path. - ``UG_TRUST_OS_SETTINGS`` skips this check and leaves precedence to the native agent. - """ - if env_bool(TRUST_OS_SETTINGS_ENV) or path is None or path.is_symlink(): - return - current_text = read_managed_file(path) - if current_text is None: - return - try: - existing = parser(current_text) - except RuntimeError: - return - # `compose` mutates its argument in place, so snapshot the on-disk state before composing. - managed_before = deepcopy(existing) - raise_on_skipped_managed_overrides( - path, - managed_file_conflicts(managed_before, compose(existing), owned_paths), - display=display, - ) - - def _unwrap(value: object) -> object: """Return a plain-Python view of a value, unwrapping tomlkit items so it compares by content.""" unwrap = getattr(value, "unwrap", None) diff --git a/tests/README.md b/tests/README.md index 1885a7585..01d4f604e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -357,11 +357,6 @@ authorization, and one-shot `sudo -n` with disconnected stdin. These are compone privileged writes replaced by temporary-file operations, not automated Isaac, real sudo-policy, or live telemetry-export coverage. -`UG_TRUST_OS_SETTINGS` component cases in those three files cover standalone and combined -opt-outs, boolean parsing, user-config generation despite conflicting managed values, and no -managed write or conflict read. Existing `UG_SKIP_OS_SETTINGS` rejection remains covered. -Native settings precedence and laptop-hosted Omnigent sessions are not live-tested here. - Managed smart defaults are covered by `test_managed_config.py`, `test_cli.py`, `test_managed_setup.py`, `test_databricks.py`, and `test_managed_budget.py`: parsing the `smart_defaults` wire field, reading older `spend_tiers` caches, skipping recommendations diff --git a/tests/integration/README.md b/tests/integration/README.md index 99b5f0395..9d9dde8e9 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -243,11 +243,6 @@ handling without sudo. This integration suite does not yet assert password-prompt counts with sudo credential caching disabled; that requires a disposable workstation/VM with an explicit sudo policy. -`UG_TRUST_OS_SETTINGS` is covered by component tests in `../test_managed_files.py`, -`../test_agent_claude.py`, and `../test_agent_codex.py`: no managed writes/checks and -continued per-user config generation. This suite does not cover the switch with live -agent settings precedence or laptop-hosted Omnigent sessions. - Caller permission-deny preservation is covered by `TestBuildClaudeArgv` in `../test_agent_claude.py`, including direct, relayed, and routing configuration output. Native parent/child enforcement and the distinction between technical search exclusion diff --git a/tests/test_agent_claude.py b/tests/test_agent_claude.py index 4f717c3c8..3dbe63784 100644 --- a/tests/test_agent_claude.py +++ b/tests/test_agent_claude.py @@ -1110,10 +1110,10 @@ def test_skip_env_bypasses_managed_file(self, monkeypatch, opt_out): assert str(claude.CLAUDE_SETTINGS_PATH) in [p for p, _ in private_writes] assert managed_writes == [] - def test_skip_env_errors_on_stale_managed_override(self, monkeypatch): + def test_skip_env_trusts_a_stale_managed_file(self, monkeypatch): + """The opt-out trusts whatever OS-managed file exists: no check, no write, no error.""" private_writes: list = [] managed_writes: list = [] - # A managed file left from an earlier workspace still points at that workspace's gateway. existing = { str(FAKE_MANAGED_PATH): { "env": { @@ -1127,18 +1127,12 @@ def test_skip_env_errors_on_stale_managed_override(self, monkeypatch): claude, "reconcile_managed_file", lambda *a, **kw: pytest.fail("must not write managed") ) state = {"workspace": WS, "codex_models": []} - # A stale override silently defeats the reconfigure, so launching must fail loudly. - with pytest.raises(RuntimeError) as excinfo: - claude.write_tool_config(state, "databricks-claude-sonnet-4") + claude.write_tool_config(state, "databricks-claude-sonnet-4") + assert str(claude.CLAUDE_SETTINGS_PATH) in [p for p, _ in private_writes] assert managed_writes == [] - message = str(excinfo.value) - assert managed_files.SKIP_OS_SETTINGS_ENV in message - assert "stale" in message - assert "ug revert" in message - @pytest.mark.parametrize("opt_out", ["user_scope", "trust", "trust_and_skip"]) - def test_opt_out_launches_over_stale_managed_override(self, monkeypatch, opt_out): + def test_user_scope_launches_over_stale_managed_override(self, monkeypatch): private_writes: list = [] managed_writes: list = [] existing = { @@ -1149,19 +1143,10 @@ def test_opt_out_launches_over_stale_managed_override(self, monkeypatch, opt_out } } self._patch(monkeypatch, private_writes, managed_writes, existing) - if opt_out == "user_scope": - managed_files.set_config_scope("user") - else: - monkeypatch.setenv(managed_files.TRUST_OS_SETTINGS_ENV, "1") - if opt_out == "trust_and_skip": - monkeypatch.setenv(managed_files.SKIP_OS_SETTINGS_ENV, "1") - monkeypatch.setattr( - claude, "reconcile_managed_file", lambda *a, **kw: pytest.fail("must not write managed") - ) + managed_files.set_config_scope("user") state = {"workspace": WS, "codex_models": []} claude.write_tool_config(state, "databricks-claude-sonnet-4") assert managed_writes == [] - assert str(claude.CLAUDE_SETTINGS_PATH) in [p for p, _ in private_writes] def test_skip_env_no_error_without_stale_override(self, monkeypatch): private_writes: list = [] diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index e7439d32f..2885ab27c 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -1771,34 +1771,27 @@ def test_skip_env_bypasses_managed_config(self, tmp_path, monkeypatch, opt_out): assert read_toml_safe(config_path)["model_provider"] == "Databricks" assert not managed_path.exists() - def test_skip_env_errors_on_stale_managed_override(self, tmp_path, monkeypatch): + def test_skip_env_trusts_a_stale_managed_file(self, tmp_path, monkeypatch): + """The opt-out trusts whatever OS-managed file exists: no check, no write, no error.""" config_path, managed_path = self._patch(tmp_path, monkeypatch) - # A managed file left from an earlier workspace still points at that workspace's gateway. managed_path.parent.mkdir(parents=True, exist_ok=True) - managed_path.write_text( + stale = ( 'model_provider = "Databricks"\n' "[model_providers.Databricks]\n" - 'base_url = "https://old-workspace.databricks.com/ai-gateway/openai/v1"\n', - encoding="utf-8", + 'base_url = "https://old-workspace.databricks.com/ai-gateway/openai/v1"\n' ) + managed_path.write_text(stale, encoding="utf-8") monkeypatch.setenv(managed_files.SKIP_OS_SETTINGS_ENV, "1") monkeypatch.setattr( codex, "reconcile_managed_file", lambda *a, **kw: pytest.fail("must not write managed") ) - # A stale override silently defeats the reconfigure, so launching must fail loudly. - with pytest.raises(RuntimeError) as excinfo: - codex.write_tool_config({"workspace": WS, "codex_models": ["gpt-5"]}) + codex.write_tool_config({"workspace": WS, "codex_models": ["gpt-5"]}) - # The stale managed file is left in place and the user is told how to clear it. - assert managed_path.exists() - message = str(excinfo.value) - assert managed_files.SKIP_OS_SETTINGS_ENV in message - assert "stale" in message - assert "ug revert" in message + assert config_path.exists() + assert managed_path.read_text(encoding="utf-8") == stale - @pytest.mark.parametrize("opt_out", ["user_scope", "trust", "trust_and_skip"]) - def test_opt_out_launches_over_stale_managed_override(self, tmp_path, monkeypatch, opt_out): + def test_user_scope_launches_over_stale_managed_override(self, tmp_path, monkeypatch): config_path, managed_path = self._patch(tmp_path, monkeypatch) managed_path.parent.mkdir(parents=True, exist_ok=True) managed_path.write_text( @@ -1807,15 +1800,7 @@ def test_opt_out_launches_over_stale_managed_override(self, tmp_path, monkeypatc 'base_url = "https://old-workspace.databricks.com/ai-gateway/openai/v1"\n', encoding="utf-8", ) - if opt_out == "user_scope": - managed_files.set_config_scope("user") - else: - monkeypatch.setenv(managed_files.TRUST_OS_SETTINGS_ENV, "1") - if opt_out == "trust_and_skip": - monkeypatch.setenv(managed_files.SKIP_OS_SETTINGS_ENV, "1") - monkeypatch.setattr( - codex, "reconcile_managed_file", lambda *a, **kw: pytest.fail("must not write managed") - ) + managed_files.set_config_scope("user") codex.write_tool_config({"workspace": WS, "codex_models": ["gpt-5"]}) diff --git a/tests/test_managed_files.py b/tests/test_managed_files.py index 703a327a8..8e8742bdf 100644 --- a/tests/test_managed_files.py +++ b/tests/test_managed_files.py @@ -724,70 +724,6 @@ def test_skip_prevents_privileged_write(self, monkeypatch): ) assert result == "unsupported" - @pytest.mark.parametrize("value", ["1", "true", "TRUE", "Yes", "on", " on "]) - def test_trust_skips_writes_and_conflict_reads(self, monkeypatch, value): - monkeypatch.delenv(managed_files.SKIP_OS_SETTINGS_ENV, raising=False) - monkeypatch.setenv(managed_files.TRUST_OS_SETTINGS_ENV, value) - assert managed_files.managed_settings_skipped() is True - assert _REAL_MANAGED_FILES_SUPPORTED() is False - monkeypatch.setattr( - managed_files, "read_managed_file", lambda *a: pytest.fail("must not check managed") - ) - managed_files.check_skipped_managed_overrides( - Path("/etc/claude-code/managed-settings.json"), - parser=json.loads, - compose=lambda settings: settings, - owned_paths=[["env", "ANTHROPIC_BASE_URL"]], - display="Claude Code", - ) - - @pytest.mark.parametrize("value", ["0", "false", "off", "no", ""]) - def test_falsey_trust_keeps_conflict_check(self, tmp_path, monkeypatch, value): - monkeypatch.delenv(managed_files.SKIP_OS_SETTINGS_ENV, raising=False) - monkeypatch.setenv(managed_files.TRUST_OS_SETTINGS_ENV, value) - assert managed_files.managed_settings_skipped() is False - path = tmp_path / "managed-settings.json" - path.write_text('{"apiKeyHelper": "old-helper"}', encoding="utf-8") - with pytest.raises(RuntimeError, match="stale and conflict"): - managed_files.check_skipped_managed_overrides( - path, - parser=json.loads, - compose=lambda settings: {"apiKeyHelper": "new-helper"}, - owned_paths=[["apiKeyHelper"]], - display="Claude Code", - ) - - def test_raise_on_skipped_managed_overrides_reports_stale_leaves(self): - with pytest.raises(RuntimeError) as excinfo: - managed_files.raise_on_skipped_managed_overrides( - Path("/etc/claude-code/managed-settings.json"), - ["env.ANTHROPIC_BASE_URL"], - display="Claude Code", - ) - message = str(excinfo.value) - assert managed_files.SKIP_OS_SETTINGS_ENV in message - assert "stale" in message - assert "ug revert" in message - - def test_user_scope_skips_managed_settings_and_files(self, monkeypatch): - monkeypatch.delenv(managed_files.SKIP_OS_SETTINGS_ENV, raising=False) - monkeypatch.setattr(managed_files, "_config_scope", "user") - assert managed_files.managed_settings_skipped() is True - monkeypatch.setattr(managed_files, "managed_files_supported", _REAL_MANAGED_FILES_SUPPORTED) - assert managed_files.managed_files_supported() is False - - def test_set_config_scope_rejects_unknown_scope(self): - with pytest.raises(ValueError, match="Unknown config scope"): - managed_files.set_config_scope("system") - - def test_raise_on_skipped_managed_overrides_silent_without_conflicts(self): - # No conflicts means nothing stale overrides the user config, so this must not raise. - managed_files.raise_on_skipped_managed_overrides( - Path("/etc/claude-code/managed-settings.json"), - [], - display="Claude Code", - ) - class TestWindowsManagedWrites: """Windows has no sudo: an elevated process writes Program Files directly, others can't."""