refactor: move install init logic into provider slices

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
JerrettDavis 2026-04-21 22:35:24 -05:00
parent ce8a2f3cf8
commit 93a1f2113f
11 changed files with 398 additions and 196 deletions

View file

@ -8,6 +8,7 @@ from collections.abc import Iterable
import click
from headroom import paths as _paths
from headroom.providers.install_registry import build_install_target_envs
from .models import (
ConfigScope,
@ -95,44 +96,9 @@ def resolve_targets(
return normalized
def _copilot_env(port: int, backend: str) -> dict[str, str]:
if backend == "anthropic":
return {
"COPILOT_PROVIDER_TYPE": "anthropic",
"COPILOT_PROVIDER_BASE_URL": f"http://127.0.0.1:{port}",
}
return {
"COPILOT_PROVIDER_TYPE": "openai",
"COPILOT_PROVIDER_BASE_URL": f"http://127.0.0.1:{port}/v1",
"COPILOT_PROVIDER_WIRE_API": "completions",
}
def build_tool_envs(port: int, backend: str, targets: list[str]) -> dict[str, dict[str, str]]:
"""Build per-target environment variables for the selected tools."""
target_envs: dict[str, dict[str, str]] = {}
if ToolTarget.CLAUDE.value in targets:
target_envs[ToolTarget.CLAUDE.value] = {
"ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}",
}
if ToolTarget.CODEX.value in targets:
target_envs[ToolTarget.CODEX.value] = {
"OPENAI_BASE_URL": f"http://127.0.0.1:{port}/v1",
}
if ToolTarget.AIDER.value in targets:
target_envs[ToolTarget.AIDER.value] = {
"OPENAI_API_BASE": f"http://127.0.0.1:{port}/v1",
"ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}",
}
if ToolTarget.COPILOT.value in targets:
target_envs[ToolTarget.COPILOT.value] = _copilot_env(port, backend)
if ToolTarget.CURSOR.value in targets:
target_envs[ToolTarget.CURSOR.value] = {
"OPENAI_BASE_URL": f"http://127.0.0.1:{port}/v1",
"ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}",
}
return target_envs
return build_install_target_envs(port, backend, targets)
def build_manifest(

View file

@ -2,23 +2,21 @@
from __future__ import annotations
import json
import os
import re
import subprocess
from pathlib import Path
import click
from headroom.providers.install_registry import (
apply_provider_scope_mutations,
revert_provider_scope_mutation,
)
from .models import ConfigScope, DeploymentManifest, ManagedMutation, ToolTarget
from .models import ConfigScope, DeploymentManifest, ManagedMutation
from .paths import (
claude_settings_path,
codex_config_path,
openclaw_config_path,
unix_system_env_targets,
unix_user_env_targets,
)
from .runtime import resolve_headroom_command
_ENV_MARKER_START = "# >>> headroom persistent env >>>"
_ENV_MARKER_END = "# <<< headroom persistent env <<<"
@ -26,12 +24,6 @@ _ENV_PATTERN = re.compile(
re.escape(_ENV_MARKER_START) + r".*?" + re.escape(_ENV_MARKER_END),
re.DOTALL,
)
_CODEX_MARKER_START = "# --- Headroom persistent provider ---"
_CODEX_MARKER_END = "# --- end Headroom persistent provider ---"
_CODEX_PATTERN = re.compile(
re.escape(_CODEX_MARKER_START) + r".*?" + re.escape(_CODEX_MARKER_END),
re.DOTALL,
)
def _merge_marker_block(file_path: Path, block: str, pattern: re.Pattern[str], marker: str) -> str:
@ -153,112 +145,6 @@ def _remove_windows_env_scope(mutations: list[ManagedMutation]) -> None:
subprocess.run(command, check=True)
def _apply_claude_provider_scope(manifest: DeploymentManifest) -> ManagedMutation:
path = claude_settings_path()
path.parent.mkdir(parents=True, exist_ok=True)
payload: dict[str, object] = {}
if path.exists():
payload = json.loads(path.read_text())
env = payload.get("env")
env_map = dict(env) if isinstance(env, dict) else {}
previous = {
name: env_map.get(name) for name in manifest.tool_envs.get(ToolTarget.CLAUDE.value, {})
}
env_map.update(manifest.tool_envs[ToolTarget.CLAUDE.value])
payload["env"] = env_map
path.write_text(json.dumps(payload, indent=2) + "\n")
return ManagedMutation(
target=ToolTarget.CLAUDE.value,
kind="json-env",
path=str(path),
data={"previous": previous},
)
def _revert_claude_provider_scope(mutation: ManagedMutation, values: dict[str, str]) -> None:
if not mutation.path:
return
path = Path(mutation.path)
if not path.exists():
return
payload = json.loads(path.read_text())
env = payload.get("env")
env_map = dict(env) if isinstance(env, dict) else {}
previous: dict[str, object] = mutation.data.get("previous", {})
for name in values:
if previous.get(name) is None:
env_map.pop(name, None)
else:
env_map[name] = previous[name]
payload["env"] = env_map
path.write_text(json.dumps(payload, indent=2) + "\n")
def _apply_codex_provider_scope(manifest: DeploymentManifest) -> ManagedMutation:
path = codex_config_path()
path.parent.mkdir(parents=True, exist_ok=True)
section = (
f"{_CODEX_MARKER_START}\n"
'model_provider = "headroom"\n\n'
"[model_providers.headroom]\n"
'name = "Headroom persistent proxy"\n'
f'base_url = "http://127.0.0.1:{manifest.port}/v1"\n'
'env_key = "OPENAI_API_KEY"\n'
"requires_openai_auth = true\n"
"supports_websockets = true\n"
f"{_CODEX_MARKER_END}\n"
)
merged = _merge_marker_block(path, section, _CODEX_PATTERN, _CODEX_MARKER_START)
path.write_text(merged)
return ManagedMutation(target=ToolTarget.CODEX.value, kind="toml-block", path=str(path))
def _revert_codex_provider_scope(mutation: ManagedMutation) -> None:
if not mutation.path:
return
path = Path(mutation.path)
if not path.exists():
return
content = path.read_text()
if _CODEX_MARKER_START not in content:
return
path.write_text(_CODEX_PATTERN.sub("", content).strip() + "\n")
def _invoke_openclaw(command: list[str]) -> None:
subprocess.run(command, check=True)
def _apply_openclaw_provider_scope(manifest: DeploymentManifest) -> ManagedMutation:
if not shutil_which("openclaw"):
raise click.ClickException("openclaw not found in PATH; cannot apply provider scope.")
command = [
*resolve_headroom_command(),
"wrap",
"openclaw",
"--no-auto-start",
"--proxy-port",
str(manifest.port),
]
_invoke_openclaw(command)
return ManagedMutation(
target=ToolTarget.OPENCLAW.value, kind="openclaw-wrap", path=str(openclaw_config_path())
)
def _revert_openclaw_provider_scope() -> None:
if not shutil_which("openclaw"):
return
command = [*resolve_headroom_command(), "unwrap", "openclaw"]
_invoke_openclaw(command)
def shutil_which(name: str) -> str | None:
from shutil import which
return which(name)
def apply_mutations(manifest: DeploymentManifest) -> list[ManagedMutation]:
"""Apply provider/user/system configuration for a deployment."""
@ -268,17 +154,10 @@ def apply_mutations(manifest: DeploymentManifest) -> list[ManagedMutation]:
mutations.extend(_apply_windows_env_scope(manifest))
else:
mutations.extend(_apply_unix_env_scope(manifest))
if ToolTarget.OPENCLAW.value in manifest.targets:
mutations.append(_apply_openclaw_provider_scope(manifest))
mutations.extend(apply_provider_scope_mutations(manifest))
return mutations
if ToolTarget.CLAUDE.value in manifest.targets:
mutations.append(_apply_claude_provider_scope(manifest))
if ToolTarget.CODEX.value in manifest.targets:
mutations.append(_apply_codex_provider_scope(manifest))
if ToolTarget.OPENCLAW.value in manifest.targets:
mutations.append(_apply_openclaw_provider_scope(manifest))
return mutations
return [*mutations, *apply_provider_scope_mutations(manifest)]
def revert_mutations(manifest: DeploymentManifest) -> None:
@ -292,11 +171,4 @@ def revert_mutations(manifest: DeploymentManifest) -> None:
_remove_unix_env_scope(shell_mutations)
for mutation in manifest.mutations:
if mutation.target == ToolTarget.CLAUDE.value and mutation.kind == "json-env":
_revert_claude_provider_scope(
mutation, manifest.tool_envs.get(ToolTarget.CLAUDE.value, {})
)
elif mutation.target == ToolTarget.CODEX.value and mutation.kind == "toml-block":
_revert_codex_provider_scope(mutation)
elif mutation.target == ToolTarget.OPENCLAW.value and mutation.kind == "openclaw-wrap":
_revert_openclaw_provider_scope()
revert_provider_scope_mutation(manifest, mutation)

View file

@ -0,0 +1,12 @@
"""Aider install-time helpers."""
from __future__ import annotations
from .runtime import build_launch_env
def build_install_env(*, port: int, backend: str) -> dict[str, str]:
"""Build the persistent install environment for Aider."""
del backend
env, _lines = build_launch_env(port=port, environ={})
return {key: env[key] for key in ("OPENAI_API_BASE", "ANTHROPIC_BASE_URL")}

View file

@ -0,0 +1,63 @@
"""Claude install-time helpers."""
from __future__ import annotations
import json
from pathlib import Path
from headroom.install.models import ConfigScope, DeploymentManifest, ManagedMutation, ToolTarget
from headroom.install.paths import claude_settings_path
from .runtime import proxy_base_url
def build_install_env(*, port: int, backend: str) -> dict[str, str]:
"""Build the persistent install environment for Claude."""
del backend
return {"ANTHROPIC_BASE_URL": proxy_base_url(port)}
def apply_provider_scope(manifest: DeploymentManifest) -> ManagedMutation | None:
"""Apply Claude provider-scope configuration when requested."""
if manifest.scope != ConfigScope.PROVIDER.value:
return None
path = claude_settings_path()
path.parent.mkdir(parents=True, exist_ok=True)
payload: dict[str, object] = {}
if path.exists():
payload = json.loads(path.read_text())
env = payload.get("env")
env_map = dict(env) if isinstance(env, dict) else {}
values = manifest.tool_envs.get(ToolTarget.CLAUDE.value, {})
previous = {name: env_map.get(name) for name in values}
env_map.update(values)
payload["env"] = env_map
path.write_text(json.dumps(payload, indent=2) + "\n")
return ManagedMutation(
target=ToolTarget.CLAUDE.value,
kind="json-env",
path=str(path),
data={"previous": previous},
)
def revert_provider_scope(mutation: ManagedMutation, manifest: DeploymentManifest) -> None:
"""Revert Claude provider-scope configuration."""
if not mutation.path:
return
path = Path(mutation.path)
if not path.exists():
return
payload = json.loads(path.read_text())
env = payload.get("env")
env_map = dict(env) if isinstance(env, dict) else {}
previous: dict[str, object] = mutation.data.get("previous", {})
values = manifest.tool_envs.get(ToolTarget.CLAUDE.value, {})
for name in values:
if previous.get(name) is None:
env_map.pop(name, None)
else:
env_map[name] = previous[name]
payload["env"] = env_map
path.write_text(json.dumps(payload, indent=2) + "\n")

View file

@ -0,0 +1,68 @@
"""Codex install-time helpers."""
from __future__ import annotations
import re
from pathlib import Path
from headroom.install.models import ConfigScope, DeploymentManifest, ManagedMutation, ToolTarget
from headroom.install.paths import codex_config_path
from .runtime import proxy_base_url
_CODEX_MARKER_START = "# --- Headroom persistent provider ---"
_CODEX_MARKER_END = "# --- end Headroom persistent provider ---"
_CODEX_PATTERN = re.compile(
re.escape(_CODEX_MARKER_START) + r".*?" + re.escape(_CODEX_MARKER_END),
re.DOTALL,
)
def build_install_env(*, port: int, backend: str) -> dict[str, str]:
"""Build the persistent install environment for Codex."""
del backend
return {"OPENAI_BASE_URL": proxy_base_url(port)}
def apply_provider_scope(manifest: DeploymentManifest) -> ManagedMutation | None:
"""Apply Codex provider-scope configuration when requested."""
if manifest.scope != ConfigScope.PROVIDER.value:
return None
path = codex_config_path()
path.parent.mkdir(parents=True, exist_ok=True)
section = (
f"{_CODEX_MARKER_START}\n"
'model_provider = "headroom"\n\n'
"[model_providers.headroom]\n"
'name = "Headroom persistent proxy"\n'
f'base_url = "{proxy_base_url(manifest.port)}"\n'
'env_key = "OPENAI_API_KEY"\n'
"requires_openai_auth = true\n"
"supports_websockets = true\n"
f"{_CODEX_MARKER_END}\n"
)
if path.exists():
existing = path.read_text()
if _CODEX_MARKER_START in existing:
merged = _CODEX_PATTERN.sub(section, existing)
else:
merged = existing.rstrip() + "\n\n" + section + "\n"
else:
merged = section + "\n"
path.write_text(merged)
return ManagedMutation(target=ToolTarget.CODEX.value, kind="toml-block", path=str(path))
def revert_provider_scope(mutation: ManagedMutation, manifest: DeploymentManifest) -> None:
"""Revert Codex provider-scope configuration."""
del manifest
if not mutation.path:
return
path = Path(mutation.path)
if not path.exists():
return
content = path.read_text()
if _CODEX_MARKER_START not in content:
return
path.write_text(_CODEX_PATTERN.sub("", content).strip() + "\n")

View file

@ -0,0 +1,25 @@
"""Copilot install-time helpers."""
from __future__ import annotations
from .wrap import build_launch_env, resolve_provider_type
def build_install_env(*, port: int, backend: str) -> dict[str, str]:
"""Build the persistent install environment for Copilot."""
provider_type = resolve_provider_type(backend, "auto", {"HEADROOM_BACKEND": backend})
env, _lines = build_launch_env(
port=port,
provider_type=provider_type,
wire_api=None,
environ={},
)
return {
key: env[key]
for key in (
"COPILOT_PROVIDER_TYPE",
"COPILOT_PROVIDER_BASE_URL",
"COPILOT_PROVIDER_WIRE_API",
)
if key in env
}

View file

@ -0,0 +1,15 @@
"""Cursor install-time helpers."""
from __future__ import annotations
from .runtime import build_proxy_targets
def build_install_env(*, port: int, backend: str) -> dict[str, str]:
"""Build the persistent install environment for Cursor."""
del backend
targets = build_proxy_targets(port)
return {
"OPENAI_BASE_URL": targets.openai_base_url,
"ANTHROPIC_BASE_URL": targets.anthropic_base_url,
}

View file

@ -0,0 +1,86 @@
"""Install-time provider registry helpers."""
from __future__ import annotations
from collections.abc import Callable
from headroom.install.models import DeploymentManifest, ManagedMutation
from headroom.providers.aider.install import build_install_env as _build_aider_install_env
from headroom.providers.claude.install import (
apply_provider_scope as _apply_claude_provider_scope,
)
from headroom.providers.claude.install import (
build_install_env as _build_claude_install_env,
)
from headroom.providers.claude.install import (
revert_provider_scope as _revert_claude_provider_scope,
)
from headroom.providers.codex.install import (
apply_provider_scope as _apply_codex_provider_scope,
)
from headroom.providers.codex.install import build_install_env as _build_codex_install_env
from headroom.providers.codex.install import (
revert_provider_scope as _revert_codex_provider_scope,
)
from headroom.providers.copilot.install import (
build_install_env as _build_copilot_install_env,
)
from headroom.providers.cursor.install import build_install_env as _build_cursor_install_env
from headroom.providers.openclaw.install import (
apply_provider_scope as _apply_openclaw_provider_scope,
)
from headroom.providers.openclaw.install import (
revert_provider_scope as _revert_openclaw_provider_scope,
)
_InstallEnvBuilder = Callable[..., dict[str, str]]
_ProviderScopeApplier = Callable[[DeploymentManifest], ManagedMutation | None]
_ProviderScopeReverter = Callable[[ManagedMutation, DeploymentManifest], None]
_ENV_BUILDERS: dict[str, _InstallEnvBuilder] = {
"claude": _build_claude_install_env,
"copilot": _build_copilot_install_env,
"codex": _build_codex_install_env,
"aider": _build_aider_install_env,
"cursor": _build_cursor_install_env,
}
_PROVIDER_SCOPE_HANDLERS: dict[str, tuple[_ProviderScopeApplier, _ProviderScopeReverter]] = {
"claude": (_apply_claude_provider_scope, _revert_claude_provider_scope),
"codex": (_apply_codex_provider_scope, _revert_codex_provider_scope),
"openclaw": (_apply_openclaw_provider_scope, _revert_openclaw_provider_scope),
}
def build_install_target_envs(
port: int, backend: str, targets: list[str]
) -> dict[str, dict[str, str]]:
"""Build per-target install environment values via provider slices."""
target_envs: dict[str, dict[str, str]] = {}
for target in targets:
builder = _ENV_BUILDERS.get(target)
if builder is None:
continue
target_envs[target] = builder(port=port, backend=backend)
return target_envs
def apply_provider_scope_mutations(manifest: DeploymentManifest) -> list[ManagedMutation]:
"""Apply provider-scope mutations owned by provider slices."""
mutations: list[ManagedMutation] = []
for target in manifest.targets:
handlers = _PROVIDER_SCOPE_HANDLERS.get(target)
if handlers is None:
continue
mutation = handlers[0](manifest)
if mutation is not None:
mutations.append(mutation)
return mutations
def revert_provider_scope_mutation(manifest: DeploymentManifest, mutation: ManagedMutation) -> None:
"""Revert a provider-scope mutation via the owning provider slice."""
handlers = _PROVIDER_SCOPE_HANDLERS.get(mutation.target)
if handlers is None:
return
handlers[1](mutation, manifest)

View file

@ -0,0 +1,50 @@
"""OpenClaw install-time helpers."""
from __future__ import annotations
import click
from headroom.install.models import DeploymentManifest, ManagedMutation, ToolTarget
from headroom.install.paths import openclaw_config_path
from headroom.install.runtime import resolve_headroom_command
def shutil_which(name: str) -> str | None:
from shutil import which
return which(name)
def _invoke_openclaw(command: list[str]) -> None:
import subprocess
subprocess.run(command, check=True)
def apply_provider_scope(manifest: DeploymentManifest) -> ManagedMutation:
"""Configure OpenClaw to route through the persistent proxy."""
if not shutil_which("openclaw"):
raise click.ClickException("openclaw not found in PATH; cannot apply provider scope.")
command = [
*resolve_headroom_command(),
"wrap",
"openclaw",
"--no-auto-start",
"--proxy-port",
str(manifest.port),
]
_invoke_openclaw(command)
return ManagedMutation(
target=ToolTarget.OPENCLAW.value,
kind="openclaw-wrap",
path=str(openclaw_config_path()),
)
def revert_provider_scope(mutation: ManagedMutation, manifest: DeploymentManifest) -> None:
"""Undo OpenClaw persistent proxy configuration."""
del mutation, manifest
if not shutil_which("openclaw"):
return
command = [*resolve_headroom_command(), "unwrap", "openclaw"]
_invoke_openclaw(command)

View file

@ -44,6 +44,41 @@ def test_build_manifest_for_persistent_docker_sets_expected_defaults() -> None:
assert "--memory" in manifest.proxy_args
def test_build_manifest_uses_provider_slice_env_builders_for_all_supported_targets() -> None:
manifest = build_manifest(
profile="default",
preset=InstallPreset.PERSISTENT_SERVICE.value,
runtime_kind="python",
scope="user",
provider_mode="manual",
targets=["claude", "copilot", "codex", "aider", "cursor"],
port=9999,
backend="anyllm",
anyllm_provider="groq",
region=None,
proxy_mode="token",
memory_enabled=False,
telemetry_enabled=True,
image="ghcr.io/chopratejas/headroom:latest",
)
assert manifest.tool_envs["claude"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:9999"
assert manifest.tool_envs["codex"]["OPENAI_BASE_URL"] == "http://127.0.0.1:9999/v1"
assert manifest.tool_envs["aider"] == {
"OPENAI_API_BASE": "http://127.0.0.1:9999/v1",
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9999",
}
assert manifest.tool_envs["cursor"] == {
"OPENAI_BASE_URL": "http://127.0.0.1:9999/v1",
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9999",
}
assert manifest.tool_envs["copilot"] == {
"COPILOT_PROVIDER_TYPE": "openai",
"COPILOT_PROVIDER_BASE_URL": "http://127.0.0.1:9999/v1",
"COPILOT_PROVIDER_WIRE_API": "completions",
}
def test_resolve_targets_provider_scope_auto_excludes_copilot(monkeypatch) -> None:
monkeypatch.setattr("headroom.install.planner.detect_targets", lambda: [])

View file

@ -1,17 +1,15 @@
from __future__ import annotations
import json
import os
from pathlib import Path
from headroom.install.models import DeploymentManifest, ManagedMutation
from headroom.install.providers import (
_apply_claude_provider_scope,
_apply_codex_provider_scope,
_apply_windows_env_scope,
_remove_windows_env_scope,
_revert_claude_provider_scope,
_revert_codex_provider_scope,
)
from headroom.install.providers import _apply_windows_env_scope, _remove_windows_env_scope
from headroom.providers.claude.install import apply_provider_scope as apply_claude_provider_scope
from headroom.providers.claude.install import revert_provider_scope as revert_claude_provider_scope
from headroom.providers.codex.install import apply_provider_scope as apply_codex_provider_scope
from headroom.providers.codex.install import revert_provider_scope as revert_codex_provider_scope
def _manifest(tmp_path: Path) -> DeploymentManifest:
@ -39,15 +37,18 @@ def test_apply_and_revert_claude_provider_scope(monkeypatch, tmp_path: Path) ->
settings_path.write_text(
json.dumps({"env": {"ANTHROPIC_API_KEY": "keep", "ANTHROPIC_BASE_URL": "https://old"}})
)
monkeypatch.setattr("headroom.install.providers.claude_settings_path", lambda: settings_path)
monkeypatch.setattr(
"headroom.providers.claude.install.claude_settings_path", lambda: settings_path
)
manifest = _manifest(tmp_path)
mutation = _apply_claude_provider_scope(manifest)
mutation = apply_claude_provider_scope(manifest)
payload = json.loads(settings_path.read_text())
assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787"
assert payload["env"]["ANTHROPIC_API_KEY"] == "keep"
_revert_claude_provider_scope(mutation, manifest.tool_envs["claude"])
assert mutation is not None
revert_claude_provider_scope(mutation, manifest)
reverted = json.loads(settings_path.read_text())
assert reverted["env"]["ANTHROPIC_BASE_URL"] == "https://old"
assert reverted["env"]["ANTHROPIC_API_KEY"] == "keep"
@ -56,15 +57,16 @@ def test_apply_and_revert_claude_provider_scope(monkeypatch, tmp_path: Path) ->
def test_apply_and_revert_codex_provider_scope(monkeypatch, tmp_path: Path) -> None:
config_path = tmp_path / "config.toml"
config_path.write_text('model = "gpt-4o"\n')
monkeypatch.setattr("headroom.install.providers.codex_config_path", lambda: config_path)
monkeypatch.setattr("headroom.providers.codex.install.codex_config_path", lambda: config_path)
manifest = _manifest(tmp_path)
mutation = _apply_codex_provider_scope(manifest)
mutation = apply_codex_provider_scope(manifest)
content = config_path.read_text()
assert 'model_provider = "headroom"' in content
assert 'base_url = "http://127.0.0.1:8787/v1"' in content
_revert_codex_provider_scope(mutation)
assert mutation is not None
revert_codex_provider_scope(mutation, manifest)
reverted = config_path.read_text()
assert 'model_provider = "headroom"' not in reverted
assert reverted.strip() == 'model = "gpt-4o"'
@ -72,25 +74,27 @@ def test_apply_and_revert_codex_provider_scope(monkeypatch, tmp_path: Path) -> N
def test_apply_openclaw_provider_scope_uses_manifest_port(monkeypatch, tmp_path: Path) -> None:
recorded: list[list[str]] = []
monkeypatch.setattr("headroom.install.providers.shutil_which", lambda name: "openclaw")
monkeypatch.setattr("headroom.providers.openclaw.install.shutil_which", lambda name: "openclaw")
monkeypatch.setattr(
"headroom.install.providers.resolve_headroom_command",
"headroom.providers.openclaw.install.resolve_headroom_command",
lambda: ["headroom"],
)
monkeypatch.setattr(
"headroom.install.providers._invoke_openclaw",
"headroom.providers.openclaw.install._invoke_openclaw",
lambda command: recorded.append(command),
)
monkeypatch.setattr(
"headroom.install.providers.openclaw_config_path",
"headroom.providers.openclaw.install.openclaw_config_path",
lambda: tmp_path / "openclaw.json",
)
manifest = _manifest(tmp_path)
manifest.port = 9999
from headroom.install.providers import _apply_openclaw_provider_scope
from headroom.providers.openclaw.install import (
apply_provider_scope as apply_openclaw_provider_scope,
)
_apply_openclaw_provider_scope(manifest)
apply_openclaw_provider_scope(manifest)
assert recorded == [["headroom", "wrap", "openclaw", "--no-auto-start", "--proxy-port", "9999"]]
@ -165,11 +169,17 @@ def test_apply_mutations_runs_openclaw_for_user_scope(monkeypatch, tmp_path: Pat
manifest.base_env = {"HEADROOM_PORT": "8787"}
manifest.tool_envs = {}
monkeypatch.setattr("headroom.install.providers.os.name", "posix")
monkeypatch.setattr("headroom.install.providers._apply_unix_env_scope", lambda deployment: [])
if os.name == "nt":
monkeypatch.setattr(
"headroom.install.providers._apply_windows_env_scope", lambda deployment: []
)
else:
monkeypatch.setattr(
"headroom.install.providers._apply_unix_env_scope", lambda deployment: []
)
monkeypatch.setattr(
"headroom.install.providers._apply_openclaw_provider_scope",
lambda deployment: ManagedMutation(target="openclaw", kind="openclaw-wrap"),
"headroom.install.providers.apply_provider_scope_mutations",
lambda deployment: [ManagedMutation(target="openclaw", kind="openclaw-wrap")],
)
from headroom.install.providers import apply_mutations