0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
204 lines
6.3 KiB
Python
204 lines
6.3 KiB
Python
"""Planner for persistent deployment manifests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
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,
|
|
DeploymentManifest,
|
|
InstallPreset,
|
|
ProviderSelectionMode,
|
|
SupervisorKind,
|
|
ToolTarget,
|
|
)
|
|
from .paths import validate_profile_name
|
|
|
|
SUPPORTED_TARGETS = [
|
|
ToolTarget.CLAUDE,
|
|
ToolTarget.COPILOT,
|
|
ToolTarget.CODEX,
|
|
ToolTarget.AIDER,
|
|
ToolTarget.CURSOR,
|
|
ToolTarget.OPENCLAW,
|
|
ToolTarget.OPENCODE,
|
|
]
|
|
PROVIDER_SCOPE_TARGETS = [
|
|
ToolTarget.CLAUDE,
|
|
ToolTarget.CODEX,
|
|
ToolTarget.OPENCLAW,
|
|
ToolTarget.OPENCODE,
|
|
]
|
|
|
|
|
|
def _binary_name(target: ToolTarget) -> str | None:
|
|
if target == ToolTarget.CURSOR:
|
|
return None
|
|
return str(target.value)
|
|
|
|
|
|
def detect_targets() -> list[str]:
|
|
"""Auto-detect available tool targets on the current host."""
|
|
|
|
detected: list[str] = []
|
|
for target in SUPPORTED_TARGETS:
|
|
binary = _binary_name(target)
|
|
if binary and shutil.which(binary):
|
|
detected.append(target.value)
|
|
continue
|
|
if target == ToolTarget.CURSOR and shutil.which("cursor"):
|
|
detected.append(target.value)
|
|
return detected
|
|
|
|
|
|
def resolve_targets(
|
|
provider_mode: str, requested_targets: Iterable[str], *, scope: str = ConfigScope.USER.value
|
|
) -> list[str]:
|
|
"""Resolve target selection according to the requested provider mode."""
|
|
|
|
valid_targets = SUPPORTED_TARGETS
|
|
if scope == ConfigScope.PROVIDER.value:
|
|
valid_targets = PROVIDER_SCOPE_TARGETS
|
|
|
|
valid = {target.value for target in valid_targets}
|
|
requested = [target.strip().lower() for target in requested_targets]
|
|
|
|
if provider_mode == ProviderSelectionMode.ALL.value:
|
|
return [target.value for target in valid_targets]
|
|
|
|
if provider_mode == ProviderSelectionMode.AUTO.value:
|
|
detected = [target for target in detect_targets() if target in valid]
|
|
return detected or [
|
|
ToolTarget.CLAUDE.value,
|
|
ToolTarget.CODEX.value,
|
|
*([] if scope == ConfigScope.PROVIDER.value else [ToolTarget.COPILOT.value]),
|
|
]
|
|
|
|
# Manual selection is the only mode that consults `requested`, so the
|
|
# provider-scope validation belongs here. Running it earlier rejected
|
|
# unsupported entries that `all`/`auto` ignore entirely — e.g.
|
|
# `install apply --scope provider --providers all --target cursor` raised
|
|
# instead of returning the provider target set.
|
|
if scope == ConfigScope.PROVIDER.value:
|
|
unsupported = [target for target in requested if target and target not in valid]
|
|
if unsupported:
|
|
unsupported_list = ", ".join(sorted(set(unsupported)))
|
|
raise click.ClickException(
|
|
"Provider scope supports only claude, codex, openclaw, and opencode; "
|
|
f"unsupported targets: {unsupported_list}"
|
|
)
|
|
|
|
normalized = []
|
|
seen: set[str] = set()
|
|
for value in requested:
|
|
if value in valid and value not in seen:
|
|
seen.add(value)
|
|
normalized.append(value)
|
|
return normalized
|
|
|
|
|
|
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."""
|
|
return build_install_target_envs(port, backend, targets)
|
|
|
|
|
|
def build_manifest(
|
|
*,
|
|
profile: str,
|
|
preset: str,
|
|
runtime_kind: str,
|
|
scope: str,
|
|
provider_mode: str,
|
|
targets: list[str],
|
|
port: int,
|
|
backend: str,
|
|
anyllm_provider: str | None,
|
|
region: str | None,
|
|
proxy_mode: str,
|
|
memory_enabled: bool,
|
|
telemetry_enabled: bool,
|
|
image: str,
|
|
no_http2: bool = False,
|
|
) -> DeploymentManifest:
|
|
"""Create a normalized deployment manifest."""
|
|
|
|
normalized_profile = validate_profile_name(profile)
|
|
|
|
if preset == InstallPreset.PERSISTENT_SERVICE.value:
|
|
supervisor_kind = SupervisorKind.SERVICE.value
|
|
elif preset == InstallPreset.PERSISTENT_TASK.value:
|
|
supervisor_kind = SupervisorKind.TASK.value
|
|
else:
|
|
supervisor_kind = SupervisorKind.NONE.value
|
|
|
|
resolved_targets = resolve_targets(provider_mode, targets, scope=scope)
|
|
tool_envs = build_tool_envs(port, backend, resolved_targets)
|
|
base_env = {
|
|
"HEADROOM_PORT": str(port),
|
|
"HEADROOM_HOST": "127.0.0.1",
|
|
"HEADROOM_MODE": proxy_mode,
|
|
"HEADROOM_BACKEND": backend,
|
|
}
|
|
if anyllm_provider:
|
|
base_env["HEADROOM_ANYLLM_PROVIDER"] = anyllm_provider
|
|
if region:
|
|
base_env["HEADROOM_REGION"] = region
|
|
# Telemetry is opt-in (off by default). Write the value explicitly so the
|
|
# generated manifest is unambiguous and doesn't depend on the runtime default.
|
|
base_env["HEADROOM_TELEMETRY"] = "on" if telemetry_enabled else "off"
|
|
if memory_enabled:
|
|
base_env["HEADROOM_MEMORY_ENABLED"] = "1"
|
|
|
|
proxy_args = [
|
|
"--host",
|
|
"127.0.0.1",
|
|
"--port",
|
|
str(port),
|
|
"--mode",
|
|
proxy_mode,
|
|
"--backend",
|
|
backend,
|
|
]
|
|
proxy_args.append("--telemetry" if telemetry_enabled else "--no-telemetry")
|
|
if memory_enabled:
|
|
proxy_args.extend(["--memory", "--memory-db-path", str(_paths.memory_db_path())])
|
|
if anyllm_provider:
|
|
proxy_args.extend(["--anyllm-provider", anyllm_provider])
|
|
if region:
|
|
proxy_args.extend(["--region", region])
|
|
if no_http2:
|
|
proxy_args.append("--no-http2")
|
|
|
|
container_name = f"headroom-{normalized_profile}"
|
|
return DeploymentManifest(
|
|
profile=normalized_profile,
|
|
preset=preset,
|
|
runtime_kind=runtime_kind,
|
|
supervisor_kind=supervisor_kind,
|
|
scope=scope,
|
|
provider_mode=provider_mode,
|
|
targets=resolved_targets,
|
|
port=port,
|
|
host="127.0.0.1",
|
|
backend=backend,
|
|
anyllm_provider=anyllm_provider,
|
|
region=region,
|
|
proxy_mode=proxy_mode,
|
|
memory_enabled=memory_enabled,
|
|
memory_db_path=str(_paths.memory_db_path()),
|
|
telemetry_enabled=telemetry_enabled,
|
|
image=image,
|
|
service_name=f"headroom-{normalized_profile}",
|
|
container_name=container_name,
|
|
health_url=f"http://127.0.0.1:{port}/readyz",
|
|
base_env=base_env,
|
|
tool_envs=tool_envs,
|
|
proxy_args=proxy_args,
|
|
)
|