Skip to content

Commit 257916d

Browse files
authored
fix(web): support persisted resolution modes (#285)
1 parent 1763a2f commit 257916d

6 files changed

Lines changed: 510 additions & 13 deletions

File tree

src/openharness/cli.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,13 +768,15 @@ def _version_callback(value: bool) -> None:
768768
plugin_app = typer.Typer(name="plugin", help="Manage plugins")
769769
auth_app = typer.Typer(name="auth", help="Manage authentication")
770770
provider_app = typer.Typer(name="provider", help="Manage provider profiles")
771+
config_app = typer.Typer(name="config", help="Show or update settings")
771772
cron_app = typer.Typer(name="cron", help="Manage cron scheduler and jobs")
772773
autopilot_app = typer.Typer(name="autopilot", help="Manage repo autopilot")
773774

774775
app.add_typer(mcp_app)
775776
app.add_typer(plugin_app)
776777
app.add_typer(auth_app)
777778
app.add_typer(provider_app)
779+
app.add_typer(config_app)
778780
app.add_typer(cron_app)
779781
app.add_typer(autopilot_app)
780782

@@ -1967,6 +1969,72 @@ def auth_copilot_logout() -> None:
19671969
print("Copilot authentication cleared.")
19681970

19691971

1972+
# ---- config subcommands ----
1973+
1974+
1975+
def _config_resolve_target(settings: object, key: str) -> tuple[object, str]:
1976+
target = settings
1977+
parts = key.split(".")
1978+
for part in parts[:-1]:
1979+
if not hasattr(target, part):
1980+
raise KeyError(key)
1981+
target = getattr(target, part)
1982+
leaf = parts[-1]
1983+
if not hasattr(target, leaf):
1984+
raise KeyError(key)
1985+
return target, leaf
1986+
1987+
1988+
def _config_coerce_value(current: object, raw: str) -> object:
1989+
if isinstance(current, bool):
1990+
lowered = raw.strip().lower()
1991+
if lowered in {"1", "true", "yes", "on"}:
1992+
return True
1993+
if lowered in {"0", "false", "no", "off"}:
1994+
return False
1995+
raise ValueError(f"Invalid boolean value: {raw}")
1996+
if isinstance(current, int) and not isinstance(current, bool):
1997+
return int(raw)
1998+
if isinstance(current, float):
1999+
return float(raw)
2000+
if isinstance(current, list):
2001+
return [entry.strip() for entry in raw.split(",") if entry.strip()]
2002+
return raw
2003+
2004+
2005+
@config_app.command("show")
2006+
def config_show() -> None:
2007+
"""Print the resolved settings JSON."""
2008+
from openharness.commands.registry import _settings_json_for_display
2009+
from openharness.config.settings import load_settings
2010+
2011+
print(_settings_json_for_display(load_settings()), flush=True)
2012+
2013+
2014+
@config_app.command("set")
2015+
def config_set(
2016+
key: str = typer.Argument(..., help="Setting key, including dotted nested keys"),
2017+
value: str = typer.Argument(..., help="Value to store"),
2018+
) -> None:
2019+
"""Persist one setting in ~/.openharness/settings.json."""
2020+
from openharness.config.settings import load_settings, save_settings
2021+
2022+
settings = load_settings()
2023+
try:
2024+
target, leaf = _config_resolve_target(settings, key)
2025+
except KeyError:
2026+
print(f"Unknown config key: {key}", file=sys.stderr)
2027+
raise typer.Exit(1)
2028+
try:
2029+
coerced = _config_coerce_value(getattr(target, leaf), value)
2030+
except ValueError as exc:
2031+
print(f"Error: {exc}", file=sys.stderr)
2032+
raise typer.Exit(1)
2033+
setattr(target, leaf, coerced)
2034+
save_settings(settings)
2035+
print(f"Updated {key}", flush=True)
2036+
2037+
19702038
# ---- provider subcommands ----
19712039

19722040

src/openharness/config/settings.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ class SandboxSettings(BaseModel):
113113
docker: DockerSandboxSettings = Field(default_factory=DockerSandboxSettings)
114114

115115

116+
class WebSettings(BaseModel):
117+
"""Outbound web tool configuration."""
118+
119+
proxy: str | None = None
120+
resolution_mode: str = "auto"
121+
synthetic_dns_cidrs: list[str] = Field(default_factory=list)
122+
123+
116124
class ProviderProfile(BaseModel):
117125
"""Named provider workflow configuration."""
118126

@@ -578,6 +586,7 @@ class Settings(BaseModel):
578586
hooks: dict[str, list[HookDefinition]] = Field(default_factory=dict)
579587
memory: MemorySettings = Field(default_factory=MemorySettings)
580588
sandbox: SandboxSettings = Field(default_factory=SandboxSettings)
589+
web: WebSettings = Field(default_factory=WebSettings)
581590
enabled_plugins: dict[str, bool] = Field(default_factory=dict)
582591
allow_project_plugins: bool = False
583592
allow_project_skills: bool = True
@@ -987,6 +996,23 @@ def _apply_env_overrides(settings: Settings) -> Settings:
987996
if sandbox_updates:
988997
updates["sandbox"] = settings.sandbox.model_copy(update=sandbox_updates)
989998

999+
web_updates: dict[str, Any] = {}
1000+
web_proxy = os.environ.get("OPENHARNESS_WEB_PROXY")
1001+
if web_proxy:
1002+
web_updates["proxy"] = web_proxy
1003+
web_resolution_mode = os.environ.get("OPENHARNESS_WEB_RESOLUTION_MODE")
1004+
if web_resolution_mode:
1005+
web_updates["resolution_mode"] = web_resolution_mode
1006+
web_synthetic_dns_cidrs = os.environ.get("OPENHARNESS_WEB_SYNTHETIC_DNS_CIDRS")
1007+
if web_synthetic_dns_cidrs:
1008+
web_updates["synthetic_dns_cidrs"] = [
1009+
entry.strip()
1010+
for entry in web_synthetic_dns_cidrs.split(",")
1011+
if entry.strip()
1012+
]
1013+
if web_updates:
1014+
updates["web"] = settings.web.model_copy(update=web_updates)
1015+
9901016
if not updates:
9911017
return settings
9921018
return settings.model_copy(update=updates)

0 commit comments

Comments
 (0)