From 228bff23f2f6da32d12633b18f0db12c9831e224 Mon Sep 17 00:00:00 2001 From: Manoj Bajaj Date: Tue, 2 Jun 2026 17:10:17 +0530 Subject: [PATCH] feat: expose proxy_mode in ClientConfig via authsome config get/set Closes #368. Adds `authsome config get proxy-mode` and `authsome config set proxy-mode ` commands. Both read/write the caller-local ClientConfig and produce JSON output consistent with the rest of the CLI. Invalid keys and invalid mode values produce structured errors with the list of valid options. Includes 10 unit tests covering all four modes, the happy-path get, persistence to disk, and both error cases. README updated with a Proxy Policy section. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 23 ++++++++++ src/authsome/cli/client_config.py | 4 +- src/authsome/cli/main.py | 56 +++++++++++++++++++++++ tests/cli/test_config.py | 75 +++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 tests/cli/test_config.py diff --git a/README.md b/README.md index d4383d27..ef394143 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,29 @@ Check my stripe balance The agent will use authsome to login into external services and perform the task. +## Proxy Policy + +By default, `authsome run` injects credentials for connected providers and lets requests to unrecognised hosts pass through (`connected_allow`). You can tighten or loosen this behaviour with the proxy-mode setting. + +```bash +# Read current mode +authsome config get proxy-mode + +# Set mode +authsome config set proxy-mode connected_deny +``` + +| Mode | Scope | Unmatched requests | +|------|-------|--------------------| +| `connected_allow` | connected providers only | pass through (**default**) | +| `connected_deny` | connected providers only | blocked with 403 | +| `configured_allow` | all configured providers (including disconnected) | pass through | +| `configured_deny` | all configured providers | blocked with 403 | + +The mode is caller-local — it lives in `~/.authsome/config.json` and applies per `authsome run` invocation. The daemon never sees or acts on it. + +--- + ## Agent Integrations Authsome ships with adapters for the most common agent frameworks and CLIs: diff --git a/src/authsome/cli/client_config.py b/src/authsome/cli/client_config.py index 7b7b3970..e1d6c0f7 100644 --- a/src/authsome/cli/client_config.py +++ b/src/authsome/cli/client_config.py @@ -24,8 +24,8 @@ class ClientConfig(BaseModel): The proxy_mode field lives here (not in ServerConfig) because the mitmproxy addon runs inside the CLI process per `authsome run` invocation. The daemon never acts on the mode itself; only the - caller-local proxy does. Users can change the mode by editing this - file directly — there is no CLI command for it today (YAGNI). + caller-local proxy does. Use `authsome config get/set proxy-mode` + to read or change the mode without hand-editing the file. """ version: str = __version__ diff --git a/src/authsome/cli/main.py b/src/authsome/cli/main.py index e4671961..7dba54e4 100644 --- a/src/authsome/cli/main.py +++ b/src/authsome/cli/main.py @@ -517,6 +517,62 @@ async def init(ctx_obj: ContextObj) -> None: ctx_obj.print_json(data) +_VALID_PROXY_MODES = ("connected_allow", "connected_deny", "configured_allow", "configured_deny") + + +@cli.group(name="config") +def config_group() -> None: + """Read and write caller-local configuration.""" + + +@config_group.command(name="get") +@click.argument("key") +@auth_command +def config_get(ctx_obj: ContextObj, key: str) -> None: + """Read a caller-local configuration value. + + Supported keys: proxy-mode + """ + from authsome.cli.client_config import load_client_config + + if key != "proxy-mode": + ctx_obj.print_json({"error": "UnknownConfigKey", "message": f"Unknown key '{key}'. Valid keys: proxy-mode"}) + sys.exit(1) + + home = Path(os.environ.get("AUTHSOME_HOME", str(Path.home() / ".authsome"))) + config = load_client_config(home) + ctx_obj.print_json({"proxy_mode": config.proxy_mode}) + + +@config_group.command(name="set") +@click.argument("key") +@click.argument("value") +@auth_command +def config_set(ctx_obj: ContextObj, key: str, value: str) -> None: + """Write a caller-local configuration value. + + Supported keys: proxy-mode + Valid proxy-mode values: connected_allow, connected_deny, configured_allow, configured_deny + """ + from authsome.cli.client_config import load_client_config, save_client_config + + if key != "proxy-mode": + ctx_obj.print_json({"error": "UnknownConfigKey", "message": f"Unknown key '{key}'. Valid keys: proxy-mode"}) + sys.exit(1) + + if value not in _VALID_PROXY_MODES: + modes_str = ", ".join(_VALID_PROXY_MODES) + ctx_obj.print_json( + {"error": "InvalidProxyMode", "message": f"Invalid proxy mode '{value}'. Valid modes: {modes_str}"} + ) + sys.exit(1) + + home = Path(os.environ.get("AUTHSOME_HOME", str(Path.home() / ".authsome"))) + updated = load_client_config(home).model_copy(update={"proxy_mode": value}) + save_client_config(home, updated) + ctx_obj.print_json({"proxy_mode": updated.proxy_mode, "status": "ok"}) + + @cli.group(name="profile") def profile() -> None: """Manage local profiles backed by identity keys.""" diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py new file mode 100644 index 00000000..8ee61727 --- /dev/null +++ b/tests/cli/test_config.py @@ -0,0 +1,75 @@ +"""Tests for `authsome config get` and `authsome config set`.""" + +import json + +import pytest + +from authsome.cli.main import cli + + +class TestConfigGet: + """Tests for `authsome config get proxy-mode`.""" + + def test_get_returns_default_mode(self, runner) -> None: + result = runner.invoke(cli, ["--log-file", "", "config", "get", "proxy-mode"]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert data["proxy_mode"] == "connected_allow" + + def test_get_reflects_saved_mode(self, runner, tmp_path, monkeypatch) -> None: + monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) + from authsome.cli.client_config import ClientConfig, save_client_config + + save_client_config(tmp_path, ClientConfig(proxy_mode="configured_deny")) + + result = runner.invoke(cli, ["--log-file", "", "config", "get", "proxy-mode"]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert data["proxy_mode"] == "configured_deny" + + def test_get_unknown_key_returns_error(self, runner) -> None: + result = runner.invoke(cli, ["--log-file", "", "config", "get", "unknown-key"]) + assert result.exit_code != 0 + data = json.loads(result.output) + assert data["error"] == "UnknownConfigKey" + assert "proxy-mode" in data["message"] + + +class TestConfigSet: + """Tests for `authsome config set proxy-mode `.""" + + @pytest.mark.parametrize( + "mode", + ["connected_allow", "connected_deny", "configured_allow", "configured_deny"], + ) + def test_set_all_valid_modes(self, runner, tmp_path, monkeypatch, mode) -> None: + monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) + + result = runner.invoke(cli, ["--log-file", "", "config", "set", "proxy-mode", mode]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert data["status"] == "ok" + assert data["proxy_mode"] == mode + + def test_set_persists_to_disk(self, runner, tmp_path, monkeypatch) -> None: + monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) + from authsome.cli.client_config import load_client_config + + runner.invoke(cli, ["--log-file", "", "config", "set", "proxy-mode", "connected_deny"]) + assert load_client_config(tmp_path).proxy_mode == "connected_deny" + + def test_set_invalid_mode_returns_error(self, runner) -> None: + result = runner.invoke(cli, ["--log-file", "", "config", "set", "proxy-mode", "bad_value"]) + assert result.exit_code != 0 + data = json.loads(result.output) + assert data["error"] == "InvalidProxyMode" + assert "bad_value" in data["message"] + for valid in ("connected_allow", "connected_deny", "configured_allow", "configured_deny"): + assert valid in data["message"] + + def test_set_unknown_key_returns_error(self, runner) -> None: + result = runner.invoke(cli, ["--log-file", "", "config", "set", "unknown-key", "value"]) + assert result.exit_code != 0 + data = json.loads(result.output) + assert data["error"] == "UnknownConfigKey" + assert "proxy-mode" in data["message"]