Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/openharness/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,13 +768,15 @@ def _version_callback(value: bool) -> None:
plugin_app = typer.Typer(name="plugin", help="Manage plugins")
auth_app = typer.Typer(name="auth", help="Manage authentication")
provider_app = typer.Typer(name="provider", help="Manage provider profiles")
config_app = typer.Typer(name="config", help="Show or update settings")
cron_app = typer.Typer(name="cron", help="Manage cron scheduler and jobs")
autopilot_app = typer.Typer(name="autopilot", help="Manage repo autopilot")

app.add_typer(mcp_app)
app.add_typer(plugin_app)
app.add_typer(auth_app)
app.add_typer(provider_app)
app.add_typer(config_app)
app.add_typer(cron_app)
app.add_typer(autopilot_app)

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


# ---- config subcommands ----


def _config_resolve_target(settings: object, key: str) -> tuple[object, str]:
target = settings
parts = key.split(".")
for part in parts[:-1]:
if not hasattr(target, part):
raise KeyError(key)
target = getattr(target, part)
leaf = parts[-1]
if not hasattr(target, leaf):
raise KeyError(key)
return target, leaf


def _config_coerce_value(current: object, raw: str) -> object:
if isinstance(current, bool):
lowered = raw.strip().lower()
if lowered in {"1", "true", "yes", "on"}:
return True
if lowered in {"0", "false", "no", "off"}:
return False
raise ValueError(f"Invalid boolean value: {raw}")
if isinstance(current, int) and not isinstance(current, bool):
return int(raw)
if isinstance(current, float):
return float(raw)
if isinstance(current, list):
return [entry.strip() for entry in raw.split(",") if entry.strip()]
return raw


@config_app.command("show")
def config_show() -> None:
"""Print the resolved settings JSON."""
from openharness.commands.registry import _settings_json_for_display
from openharness.config.settings import load_settings

print(_settings_json_for_display(load_settings()), flush=True)


@config_app.command("set")
def config_set(
key: str = typer.Argument(..., help="Setting key, including dotted nested keys"),
value: str = typer.Argument(..., help="Value to store"),
) -> None:
"""Persist one setting in ~/.openharness/settings.json."""
from openharness.config.settings import load_settings, save_settings

settings = load_settings()
try:
target, leaf = _config_resolve_target(settings, key)
except KeyError:
print(f"Unknown config key: {key}", file=sys.stderr)
raise typer.Exit(1)
try:
coerced = _config_coerce_value(getattr(target, leaf), value)
except ValueError as exc:
print(f"Error: {exc}", file=sys.stderr)
raise typer.Exit(1)
setattr(target, leaf, coerced)
save_settings(settings)
print(f"Updated {key}", flush=True)


# ---- provider subcommands ----


Expand Down
26 changes: 26 additions & 0 deletions src/openharness/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ class SandboxSettings(BaseModel):
docker: DockerSandboxSettings = Field(default_factory=DockerSandboxSettings)


class WebSettings(BaseModel):
"""Outbound web tool configuration."""

proxy: str | None = None
resolution_mode: str = "auto"
synthetic_dns_cidrs: list[str] = Field(default_factory=list)


class ProviderProfile(BaseModel):
"""Named provider workflow configuration."""

Expand Down Expand Up @@ -578,6 +586,7 @@ class Settings(BaseModel):
hooks: dict[str, list[HookDefinition]] = Field(default_factory=dict)
memory: MemorySettings = Field(default_factory=MemorySettings)
sandbox: SandboxSettings = Field(default_factory=SandboxSettings)
web: WebSettings = Field(default_factory=WebSettings)
enabled_plugins: dict[str, bool] = Field(default_factory=dict)
allow_project_plugins: bool = False
allow_project_skills: bool = True
Expand Down Expand Up @@ -987,6 +996,23 @@ def _apply_env_overrides(settings: Settings) -> Settings:
if sandbox_updates:
updates["sandbox"] = settings.sandbox.model_copy(update=sandbox_updates)

web_updates: dict[str, Any] = {}
web_proxy = os.environ.get("OPENHARNESS_WEB_PROXY")
if web_proxy:
web_updates["proxy"] = web_proxy
web_resolution_mode = os.environ.get("OPENHARNESS_WEB_RESOLUTION_MODE")
if web_resolution_mode:
web_updates["resolution_mode"] = web_resolution_mode
web_synthetic_dns_cidrs = os.environ.get("OPENHARNESS_WEB_SYNTHETIC_DNS_CIDRS")
if web_synthetic_dns_cidrs:
web_updates["synthetic_dns_cidrs"] = [
entry.strip()
for entry in web_synthetic_dns_cidrs.split(",")
if entry.strip()
]
if web_updates:
updates["web"] = settings.web.model_copy(update=web_updates)

if not updates:
return settings
return settings.model_copy(update=updates)
Expand Down
Loading
Loading