Files
Unraid-AppOps-Gateway-Public/backend/app/services/deployment_broker.py
T

1063 lines
46 KiB
Python

"""Client for the private, typed deployment broker surface.
The client sends a canonical DeploymentPlanV1 or a closed fixed-target
operation to a configured controller. It neither exposes a command field nor
calls a Docker socket/Portainer exec endpoint. The controller independently
validates repository, revision and runtime topology before it can mutate
production.
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import ipaddress
import json
import os
import re
import stat
import time
import uuid
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
import httpx
from ..core.config import settings
from ..core.redaction import redact_message
from ..schemas.deployment import DeploymentPlanV1
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
from ..schemas.forge_bootstrap_apply import ForgeBootstrapApplyV1
from ..schemas.forge_runtime_readiness import ForgeRuntimeReadinessV1
_REQUEST_ID = re.compile(r"^[0-9a-f]{64}$")
_MAX_DELIVERY_STATUS_BYTES = 512 * 1024
_MAX_RUNNER_FLEET_STATUS_BYTES = 512 * 1024
_MAX_CONTROLLER_HEALTH_BYTES = 64 * 1024
_WORKMESH_REVISION = re.compile(r"^[0-9a-f]{40}$")
_WORKMESH_PREFLIGHT_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_WORKMESH_IDEMPOTENCY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
_FORGE_SECRET_DELIVERY_PATH = "/v1/appops/forge/bootstrap/secrets"
_MAX_FORGE_SECRET_DELIVERY_BODY_BYTES = 16 * 1024
_MAX_FORGE_SECRET_DELIVERY_RESPONSE_BYTES = 64 * 1024
_FORGE_RUNTIME_READINESS_PATH = "/v1/appops/forge/bootstrap/readiness/"
_MAX_FORGE_RUNTIME_READINESS_RESPONSE_BYTES = 32 * 1024
_FORGE_RUNTIME_READINESS_TOTAL_SECONDS = 35.0
_FORGE_RUNTIME_READINESS_READ_SECONDS = 20.0
def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate_json_key")
result[key] = value
return result
class DeploymentBrokerClient:
"""Minimal HMAC client for the controller's versioned deployment API."""
def __init__(
self,
*,
base_url: str | None = None,
verify_tls: bool | None = None,
ca_file: str | None = None,
):
configured_url = base_url or settings.apis.get("DEPLOYMENT_BROKER_URL")
self.base_url = str(configured_url or "").strip().rstrip("/")
self.verify_tls = (
bool(settings.apis.get("DEPLOYMENT_BROKER_VERIFY_TLS"))
if verify_tls is None
else verify_tls
)
configured_ca_file = (
settings.apis.get("DEPLOYMENT_BROKER_CA_FILE")
if ca_file is None
else ca_file
)
self.ca_file = str(configured_ca_file or "").strip() or None
self.timeout_seconds = max(
2.0,
min(30.0, float(settings.apis.get("DEPLOYMENT_BROKER_TIMEOUT_SECONDS") or 10)),
)
def configuration_error(self) -> str | None:
if not self.base_url:
return "deployment_broker_url_missing"
parsed = urlparse(self.base_url)
loopback_http = False
if parsed.scheme == "http":
hostname = parsed.hostname or ""
try:
loopback_http = ipaddress.ip_address(hostname).is_loopback
except ValueError:
loopback_http = hostname.lower() == "localhost"
if (
not (parsed.scheme == "https" or loopback_http)
or not parsed.netloc
or parsed.username
or parsed.password
or parsed.query
or parsed.fragment
):
return "deployment_broker_url_invalid"
if parsed.scheme == "https":
if not self.verify_tls:
return "deployment_broker_tls_verification_required"
if self.ca_file and not self._trusted_ca_file_is_safe(self.ca_file):
return "deployment_broker_ca_file_invalid"
return None
@staticmethod
def _trusted_ca_file_is_safe(value: str) -> bool:
"""Require an AppOps-owned readable CA bundle that others cannot write."""
path = Path(value)
if not path.is_absolute():
return False
try:
info = path.lstat()
except OSError:
return False
if (
path.is_symlink()
or not stat.S_ISREG(info.st_mode)
or not 1 <= info.st_size <= 1024 * 1024
):
return False
if os.name != "nt":
return (
info.st_uid == os.geteuid()
and not (stat.S_IMODE(info.st_mode) & 0o022)
and os.access(path, os.R_OK)
)
return True
def is_configured(self) -> bool:
return self.configuration_error() is None
def _verify_option(self) -> bool | str:
parsed = urlparse(self.base_url)
if parsed.scheme == "https" and self.ca_file:
return self.ca_file
return self.verify_tls
@staticmethod
def _canonical_payload(payload: dict[str, Any]) -> bytes:
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
@staticmethod
def _signature(
secret: str,
method: str,
path: str,
timestamp: str,
nonce: str,
body: bytes,
) -> str:
"""Bind the HMAC to one versioned HTTP request, not merely its body."""
body_digest = hashlib.sha256(body).hexdigest()
signed = (
f"itworx-controller-request/v2\n{method.upper()}\n{path}\n"
f"{timestamp}\n{nonce}\n{body_digest}"
).encode("ascii")
return hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
def _request(
self,
method: str,
path: str,
*,
secret: str,
payload: dict[str, Any] | None = None,
error_response_fields: frozenset[str] = frozenset(),
request_timeout: httpx.Timeout | None = None,
) -> dict[str, Any]:
if not self.is_configured():
return {"status": "missing_config", "reason": self.configuration_error()}
if not secret or len(secret) < 32:
return {"status": "missing_secret", "reason": "deployment_broker_secret_missing_or_weak"}
body = self._canonical_payload(payload) if payload is not None else b""
timestamp = str(int(time.time()))
nonce = uuid.uuid4().hex
headers = {
"Accept": "application/json",
"X-ITWorx-Deployment-Timestamp": timestamp,
"X-ITWorx-Deployment-Nonce": nonce,
"X-ITWorx-Deployment-Signature": self._signature(
secret, method, path, timestamp, nonce, body
),
}
if payload is not None:
headers["Content-Type"] = "application/json"
try:
with httpx.Client(
timeout=request_timeout or self.timeout_seconds,
verify=self._verify_option(),
) as client:
response = client.request(method, f"{self.base_url}{path}", content=body, headers=headers)
except httpx.RequestError as exc:
return {"status": "unreachable", "reason": redact_message(str(exc))}
if response.status_code in {401, 403}:
rejected = {"status": "unauthorized", "reason": "deployment_broker_authentication_failed"}
try:
auth_payload = response.json()
except ValueError:
auth_payload = None
if ("noMutation" in error_response_fields and isinstance(auth_payload, dict)
and isinstance(auth_payload.get("noMutation"), bool)):
rejected["noMutation"] = auth_payload["noMutation"]
return rejected
try:
decoded = response.json()
except ValueError:
decoded = {}
if not isinstance(decoded, dict):
decoded = {}
if response.status_code >= 400:
rejected = {
"status": str(decoded.get("status") or "rejected"),
"reason": redact_message(str(decoded.get("reason") or f"deployment_broker_http_{response.status_code}")),
}
for field in error_response_fields:
value = decoded.get(field)
if isinstance(value, str) and len(value) <= 256 or field == "noMutation" and isinstance(value, bool):
rejected[field] = value
return rejected
return {"status": str(decoded.get("status") or "ok"), **decoded}
def controller_health(self) -> dict[str, Any]:
"""Read the bounded root-controller health and release identity."""
if not self.is_configured():
return {"status": "missing_config", "reason": self.configuration_error()}
try:
with httpx.Client(
timeout=self.timeout_seconds,
verify=self._verify_option(),
follow_redirects=False,
) as client, client.stream(
"GET",
f"{self.base_url}/health",
headers={"Accept": "application/json"},
) as response:
if response.status_code != 200:
return {
"status": "unavailable",
"reason": f"controller_health_http_{response.status_code}",
}
body = bytearray()
for chunk in response.iter_bytes():
body.extend(chunk)
if len(body) > _MAX_CONTROLLER_HEALTH_BYTES:
return {
"status": "unavailable",
"reason": "controller_health_response_too_large",
}
except httpx.RequestError as exc:
return {"status": "unreachable", "reason": redact_message(str(exc))}
try:
decoded = json.loads(bytes(body).decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return {"status": "unavailable", "reason": "controller_health_invalid_response"}
return (
decoded
if isinstance(decoded, dict)
else {"status": "unavailable", "reason": "controller_health_invalid_response"}
)
def preflight(self, plan: DeploymentPlanV1, *, secret: str) -> dict[str, Any]:
return self._request(
"POST",
"/v1/appops/deployments/preflight",
secret=secret,
payload={"plan": plan.model_dump(mode="json")},
)
def appops_self_upgrade_preflight(
self, revision: str, reason: str, expected_current_revision: str | None,
) -> dict[str, Any]:
"""Ask the private controller to validate one fixed protected-main target."""
return self._request(
"POST", "/v1/appops/self-upgrade/preflight",
secret=settings.mcp_token or "",
payload={"repository": "jens/unraid-appops-gateway", "revision": revision,
"reason": reason, "expectedCurrentRevision": expected_current_revision},
)
def hub_component_operation(self, stage: str, payload: dict[str, Any]) -> dict[str, Any]:
"""Only closed, signed component-promotion endpoints; never caller commands."""
if stage not in {"baseline", "preflight", "apply", "status"}:
raise ValueError("component promotion stage is invalid")
return self._request(
"POST", f"/v1/appops/hub-components/{stage}",
secret=settings.mcp_token or "", payload=payload,
error_response_fields=frozenset({"planId", "planDigest", "noMutation"}),
)
def hub_auto_apply(self, payload: dict[str, Any], *, preflight: bool = False) -> dict[str, Any]:
return self._request(
"POST", "/v1/appops/registered-targets/auto-apply" + ("/preflight" if preflight else ""),
secret=settings.mcp_token or "", payload=payload,
error_response_fields=frozenset({"noMutation"}),
)
def projectbrain_delivery_operation(self, stage: str, payload: dict[str, Any]) -> dict[str, Any]:
"""Use the fixed ProjectBrain retry gate; callers cannot select a repository or runtime."""
if stage not in {"preflight", "revalidate"} or not isinstance(payload, dict):
raise ValueError("ProjectBrain delivery stage is invalid")
if stage == "preflight":
if (set(payload) != {"revision"} or not isinstance(payload.get("revision"), str)
or not re.fullmatch(r"[0-9a-f]{40}", payload["revision"])):
raise ValueError("ProjectBrain preflight contract is invalid")
elif (set(payload) != {"revision", "expectedPreflightSha256", "idempotencyKey"}
or not isinstance(payload.get("revision"), str)
or not re.fullmatch(r"[0-9a-f]{40}", payload["revision"])
or not isinstance(payload.get("expectedPreflightSha256"), str)
or not re.fullmatch(r"[0-9a-f]{64}", payload["expectedPreflightSha256"])
or not isinstance(payload.get("idempotencyKey"), str)
or not re.fullmatch(r"[A-Za-z0-9._:-]{8,128}", payload["idempotencyKey"])):
raise ValueError("ProjectBrain revalidation contract is invalid")
return self._request(
"POST", f"/v1/appops/projectbrain-delivery/{stage}",
secret=settings.mcp_token or "", payload=payload,
error_response_fields=frozenset({"noMutation"}),
)
def hub_delivery_operation(self, stage: str, payload: dict[str, Any]) -> dict[str, Any]:
"""Use the fixed Hub signed-delivery gate without retrying uncertain requests."""
if stage not in {"preflight", "revalidate"} or not isinstance(payload, dict):
raise ValueError("Hub delivery stage is invalid")
if stage == "preflight":
if (set(payload) != {"revision"} or not isinstance(payload.get("revision"), str)
or not re.fullmatch(r"[0-9a-f]{40}", payload["revision"])):
raise ValueError("Hub delivery preflight contract is invalid")
elif (set(payload) != {"revision", "expectedPreflightSha256", "idempotencyKey"}
or not isinstance(payload.get("revision"), str)
or not re.fullmatch(r"[0-9a-f]{40}", payload["revision"])
or not isinstance(payload.get("expectedPreflightSha256"), str)
or not re.fullmatch(r"[0-9a-f]{64}", payload["expectedPreflightSha256"])
or not isinstance(payload.get("idempotencyKey"), str)
or not re.fullmatch(r"[A-Za-z0-9._:-]{8,128}", payload["idempotencyKey"])):
raise ValueError("Hub delivery revalidation contract is invalid")
result = self._request(
"POST", f"/v1/appops/hub-delivery/{stage}",
secret=settings.mcp_token or "", payload=payload,
error_response_fields=frozenset({"noMutation"}),
)
if stage == "revalidate" and (
result.get("status") == "unreachable" or type(result.get("noMutation")) is not bool
):
return {
"status": "unknown",
"reason": "hub_delivery_outcome_unknown",
"noMutation": False,
}
if stage == "preflight" and result.get("status") == "unreachable":
return {
"status": "unavailable",
"reason": "hub_delivery_preflight_unavailable",
"noMutation": True,
}
if stage == "preflight" and type(result.get("noMutation")) is not bool:
return {
"status": "unavailable",
"reason": "hub_delivery_preflight_response_invalid",
"noMutation": True,
}
if stage == "preflight" and result.get("noMutation") is not True:
return {
"status": "unknown",
"reason": "hub_delivery_preflight_mutation_unexpected",
"noMutation": False,
}
return result
def registered_legacy_operation(self, stage: str, payload: dict[str, Any]) -> dict[str, Any]:
"""Signed fixed-stage metadata adoption/recovery for existing registered targets."""
if stage not in {"observe", "baseline", "preflight", "adopt", "status", "rollback", "delivery"}:
raise ValueError("registered legacy stage is invalid")
if stage == "observe" and (not isinstance(payload, dict) or set(payload) != {"repository"}):
raise ValueError("registered observation contract is invalid")
if stage == "baseline":
if not isinstance(payload, dict):
raise ValueError("registered capture contract is invalid")
base = {"repository", "requestId"}
bindings = {"expectedSourceFingerprint", "expectedRuntimeFingerprint", "expectedStateSha256", "expectedContainerIds"}
capture = payload.get("captureRuntimeEnvironment", False)
if (type(capture) is not bool or (capture is False and
set(payload) not in (base, base | {"captureRuntimeEnvironment"}))):
raise ValueError("registered capture contract is invalid")
if capture is True:
if (set(payload) != base | bindings | {"captureRuntimeEnvironment"}
or any(not isinstance(payload.get(field), str) or not re.fullmatch(r"[0-9a-f]{64}", payload[field])
for field in bindings - {"expectedContainerIds"})
or not isinstance(payload.get("expectedContainerIds"), dict)
or not 1 <= len(payload["expectedContainerIds"]) <= 64
or any(not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", name)
or not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value)
for name, value in payload["expectedContainerIds"].items())):
raise ValueError("registered capture contract is invalid")
return self._request(
"POST", f"/v1/appops/registered-legacy/{stage}",
secret=settings.mcp_token or "", payload=payload,
error_response_fields=frozenset({"noMutation"}),
)
def appops_self_upgrade_apply(
self, revision: str, reason: str, expected_current_revision: str | None,
preflight_sha256: str, idempotency_key: str,
) -> dict[str, Any]:
return self._request(
"POST", "/v1/appops/self-upgrade/apply",
secret=settings.mcp_token or "",
payload={"repository": "jens/unraid-appops-gateway", "revision": revision,
"reason": reason, "expectedCurrentRevision": expected_current_revision,
"preflightSha256": preflight_sha256, "idempotencyKey": idempotency_key},
)
def appops_self_upgrade_status(self, operation_id: str) -> dict[str, Any]:
if not re.fullmatch(r"[0-9a-f]{64}", operation_id):
return {"status": "invalid_operation_id"}
return self._request(
"GET", f"/v1/appops/self-upgrade/status/{operation_id}",
secret=settings.mcp_token or "",
)
def apply(self, plan: DeploymentPlanV1, *, secret: str, idempotency_key: str) -> dict[str, Any]:
return self._request(
"POST",
"/v1/appops/deployments/apply",
secret=secret,
payload={"plan": plan.model_dump(mode="json"), "idempotencyKey": idempotency_key},
)
def rollback(self, plan: DeploymentPlanV1, *, secret: str, idempotency_key: str) -> dict[str, Any]:
return self._request(
"POST",
"/v1/appops/deployments/rollback",
secret=secret,
payload={"plan": plan.model_dump(mode="json"), "idempotencyKey": idempotency_key},
)
def enrollment_preflight(self, enrollment: dict[str, Any], *, secret: str) -> dict[str, Any]:
"""Validate a bounded target enrollment with the controller, read-only."""
return self._request(
"POST",
"/v1/appops/enrollments/preflight",
secret=secret,
payload={"enrollment": enrollment},
)
def forge_bootstrap_preflight(
self, profile: ForgeBootstrapPreflightV1, *, secret: str
) -> dict[str, Any]:
"""Read fixed Forge source and host prerequisites through bootstrap HMAC."""
return self._request(
"POST",
"/v1/appops/forge/bootstrap/preflight",
secret=secret,
payload={"profile": profile.model_dump(mode="json")},
)
def forge_bootstrap_apply(
self,
profile: ForgeBootstrapApplyV1,
*,
idempotency_key: str,
secret: str,
) -> dict[str, Any]:
"""Submit only the fixed, preflight-bound Forge first-install profile."""
result = self._request(
"POST",
"/v1/appops/forge/bootstrap/apply",
secret=secret,
payload={
"profile": profile.model_dump(mode="json"),
"idempotencyKey": idempotency_key,
},
error_response_fields=frozenset({
"apiVersion", "request", "repository", "revision", "operation",
"workspaceImageDigest", "frontdoorImageDigest", "noMutation",
}),
)
if result.get("status") == "preflight_changed":
result.pop("reason", None)
return result
def forge_secret_delivery(
self,
profile: ForgeBootstrapPreflightV1,
*,
config_bytes: bytes,
oidc_client_secret: bytes,
authentik_read_token: bytes,
secret: str,
) -> dict[str, Any]:
"""Send the single fixed Forge credential bundle request.
Any response after transmission is treated as an unknown outcome unless
it is a bounded, strict JSON object. Caller-side validation still owns
the complete success contract and never echoes this internal response.
"""
configuration_error = self.configuration_error()
if configuration_error is not None:
return {
"status": "missing_config",
"reason": "forge_secret_delivery_broker_configuration_invalid",
}
if (
not isinstance(secret, str)
or len(secret) < 32
or not secret.isascii()
or not secret.isprintable()
or not isinstance(config_bytes, bytes)
or not 1 <= len(config_bytes) <= 8192
or not isinstance(oidc_client_secret, bytes)
or not 32 <= len(oidc_client_secret) <= 255
or any(byte < 0x20 or byte > 0x7E for byte in oidc_client_secret)
or not isinstance(authentik_read_token, bytes)
or not 32 <= len(authentik_read_token) <= 256
or any(byte < 0x21 or byte > 0x7E for byte in authentik_read_token)
):
return {"status": "missing_secret", "reason": "forge_secret_delivery_inputs_invalid"}
payload = {
"apiVersion": "itworx.forge-secret-delivery/v1",
"target": "itworx-forge",
"profile": profile.model_dump(mode="json"),
"configBase64": base64.b64encode(config_bytes).decode("ascii"),
"oidcClientSecretBase64": base64.b64encode(oidc_client_secret).decode("ascii"),
"authentikReadTokenBase64": base64.b64encode(authentik_read_token).decode("ascii"),
}
body = self._canonical_payload(payload)
payload.clear()
if len(body) > _MAX_FORGE_SECRET_DELIVERY_BODY_BYTES:
return {"status": "missing_secret", "reason": "forge_secret_delivery_request_too_large"}
timestamp = str(int(time.time()))
nonce = uuid.uuid4().hex
path = _FORGE_SECRET_DELIVERY_PATH
headers = {
"Accept": "application/json",
"Accept-Encoding": "identity",
"Content-Type": "application/json",
"X-ITWorx-Deployment-Timestamp": timestamp,
"X-ITWorx-Deployment-Nonce": nonce,
"X-ITWorx-Deployment-Signature": self._signature(
secret, "POST", path, timestamp, nonce, body
),
}
response_body = bytearray()
try:
with httpx.Client(
timeout=self.timeout_seconds,
verify=self._verify_option(),
follow_redirects=False,
) as client, client.stream(
"POST",
f"{self.base_url}{path}",
content=body,
headers=headers,
) as response:
if response.status_code != 200:
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_controller_rejected",
}
content_length = response.headers.get("content-length")
if content_length is not None and (
not content_length.isascii()
or not content_length.isdecimal()
or len(content_length) > 10
or int(content_length) > _MAX_FORGE_SECRET_DELIVERY_RESPONSE_BYTES
):
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_response_invalid",
}
for chunk in response.iter_bytes(chunk_size=16 * 1024):
response_body.extend(chunk)
if len(response_body) > _MAX_FORGE_SECRET_DELIVERY_RESPONSE_BYTES:
response_body.clear()
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_response_invalid",
}
except Exception: # noqa: BLE001 - exception text can contain held credentials.
response_body.clear()
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_transport_unknown",
}
try:
decoded = json.loads(
bytes(response_body).decode("utf-8"),
object_pairs_hook=_unique_json_object,
)
except (UnicodeError, ValueError, TypeError, RecursionError):
response_body.clear()
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_response_invalid",
}
response_body.clear()
if not isinstance(decoded, dict):
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_response_invalid",
}
return {"status": "response", "response": decoded}
def forge_bootstrap_resume(self, request_id: str, *, secret: str) -> dict[str, Any]:
"""Resume only one recorded pre-migration credentials blocker."""
if not _REQUEST_ID.fullmatch(request_id):
return {"status": "invalid_request_id"}
result = self._request(
"POST",
f"/v1/appops/forge/bootstrap/resume/{request_id}",
secret=secret,
payload={},
error_response_fields=frozenset({
"apiVersion", "request", "repository", "revision", "operation",
"workspaceImageDigest", "frontdoorImageDigest", "noMutation",
}),
)
if result.get("status") == "preflight_changed":
result.pop("reason", None)
return result
def forge_bootstrap_status(self, request_id: str, *, secret: str) -> dict[str, Any]:
"""Read one fixed Forge first-install request; never submits mutations."""
if not _REQUEST_ID.fullmatch(request_id):
return {"status": "invalid_request_id"}
return self._request(
"GET",
f"/v1/appops/forge/bootstrap/status/{request_id}",
secret=secret,
)
def forge_bootstrap_readiness(
self, request_id: str, *, secret: str
) -> dict[str, Any]:
"""Fetch one fresh fixed-profile runtime snapshot with no receipt fallback."""
if not _REQUEST_ID.fullmatch(request_id):
return {"status": "unavailable"}
if (
not self.is_configured()
or not isinstance(secret, str)
or len(secret) < 32
or not secret.isascii()
or not secret.isprintable()
):
return {"status": "unavailable"}
timestamp = str(int(time.time()))
nonce = uuid.uuid4().hex
path = f"{_FORGE_RUNTIME_READINESS_PATH}{request_id}"
headers = {
"Accept": "application/json",
"Accept-Encoding": "identity",
"Cache-Control": "no-store",
"Pragma": "no-cache",
"X-ITWorx-Deployment-Timestamp": timestamp,
"X-ITWorx-Deployment-Nonce": nonce,
"X-ITWorx-Deployment-Signature": self._signature(
secret, "GET", path, timestamp, nonce, b""
),
}
response_body = bytearray()
deadline = time.monotonic() + _FORGE_RUNTIME_READINESS_TOTAL_SECONDS
client_timeout = httpx.Timeout(
connect=min(self.timeout_seconds, 5.0),
read=_FORGE_RUNTIME_READINESS_READ_SECONDS,
write=min(self.timeout_seconds, 5.0),
pool=min(self.timeout_seconds, 5.0),
)
try:
with httpx.Client(
timeout=client_timeout,
verify=self._verify_option(),
follow_redirects=False,
) as client, client.stream(
"GET",
f"{self.base_url}{path}",
content=b"",
headers=headers,
) as response:
if response.status_code != 200:
return {"status": "unavailable"}
content_lengths = response.headers.get_list("content-length")
if len(content_lengths) > 1:
return {"status": "unavailable"}
content_length = content_lengths[0] if content_lengths else None
if content_length is not None and (
not content_length.isascii()
or not content_length.isdecimal()
or len(content_length) > 10
or int(content_length) > _MAX_FORGE_RUNTIME_READINESS_RESPONSE_BYTES
):
return {"status": "unavailable"}
content_encoding = response.headers.get("content-encoding", "identity").lower()
content_type = response.headers.get("content-type", "").split(";", 1)[0].strip().lower()
if content_encoding != "identity" or content_type != "application/json":
return {"status": "unavailable"}
for chunk in response.iter_raw():
if time.monotonic() > deadline:
response_body.clear()
return {"status": "unavailable"}
response_body.extend(chunk)
if len(response_body) > _MAX_FORGE_RUNTIME_READINESS_RESPONSE_BYTES:
response_body.clear()
return {"status": "unavailable"}
except Exception: # noqa: BLE001 - transport diagnostics may contain held credentials.
response_body.clear()
return {"status": "unavailable"}
if time.monotonic() > deadline:
response_body.clear()
return {"status": "unavailable"}
try:
decoded = json.loads(
bytes(response_body).decode("utf-8", errors="strict"),
object_pairs_hook=_unique_json_object,
)
validated = ForgeRuntimeReadinessV1.model_validate(decoded)
except (UnicodeError, ValueError, TypeError, RecursionError):
response_body.clear()
return {"status": "unavailable"}
response_body.clear()
now = int(time.time())
if (
validated.request != request_id
or validated.nonce != nonce
or validated.observed_at < now - 30
or validated.observed_at > now + 5
):
return {"status": "unavailable"}
return validated.model_dump(mode="json", by_alias=True)
def enrollment_capabilities(self, *, secret: str) -> dict[str, Any]:
"""Read the bootstrap-protected enrollment controller capability."""
return self._request(
"POST",
"/v1/appops/enrollments/capabilities",
secret=secret,
payload={},
)
def initialize_managed_appdata(self, *, secret: str) -> dict[str, Any]:
"""Initialize only the fixed managed storage root through bootstrap HMAC."""
return self._request(
"POST",
"/v1/appops/managed-storage/initialize",
secret=secret,
payload={
"apiVersion": "itworx.managed-storage-initialize/v1",
"target": "tower/deployment-controller",
},
)
def repopulse_adoption_capabilities(self, *, secret: str) -> dict[str, Any]:
"""Read fixed RepoPulse adoption readiness through bootstrap HMAC v2."""
return self._request(
"POST",
"/v1/appops/repopulse/adoption/capabilities",
secret=secret,
payload={},
)
def repopulse_adoption_reserve(
self,
*,
secret: str,
intent_id: str,
idempotency_sha256: str,
preflight_sha256: str,
) -> dict[str, Any]:
"""Reserve the fixed controller intent through bootstrap HMAC v2."""
return self._request(
"POST",
"/v1/appops/repopulse/adoption/reserve",
secret=secret,
payload={
"intentId": intent_id,
"idempotencySha256": idempotency_sha256,
"preflightSha256": preflight_sha256,
},
)
def repopulse_adoption_status(self, *, secret: str) -> dict[str, Any]:
"""Read sanitized fixed adoption state through bootstrap HMAC v2."""
return self._request(
"GET",
"/v1/appops/repopulse/adoption/status",
secret=secret,
)
def repopulse_adoption_execute(
self,
*,
secret: str,
intent_id: str,
idempotency_sha256: str,
preflight_sha256: str,
preflight_projection: dict[str, Any],
target_credential: str,
) -> dict[str, Any]:
"""Request the one fixed migration; the controller owns every effect."""
if urlparse(self.base_url).scheme != "https" or self.configuration_error() is not None:
return {
"status": "missing_config",
"reason": "repopulse_adoption_private_tls_required",
}
if (
not isinstance(target_credential, str)
or not 32 <= len(target_credential) <= 256
or not target_credential.isascii()
or not target_credential.isprintable()
):
return {"status": "invalid", "reason": "repopulse_target_credential_invalid"}
return self._request(
"POST",
"/v1/appops/repopulse/adoption/execute",
secret=secret,
payload={
"intentId": intent_id,
"idempotencySha256": idempotency_sha256,
"preflightSha256": preflight_sha256,
"preflightProjection": preflight_projection,
"targetCredential": target_credential,
},
)
def enroll_target(
self,
enrollment: dict[str, Any],
*,
secret: str,
idempotency_key: str,
) -> dict[str, Any]:
"""Create exactly one controller-owned target through bootstrap HMAC."""
return self._request(
"POST",
"/v1/appops/enrollments",
secret=secret,
payload={"enrollment": enrollment, "idempotencyKey": idempotency_key},
)
def configure_enrollment_runtime(
self,
runtime: dict[str, Any],
*,
secret: str,
) -> dict[str, Any]:
"""Write profile-owned runtime values using that target's own HMAC."""
return self._request(
"POST",
"/v1/appops/enrollments/runtime",
secret=secret,
payload={"runtime": runtime},
)
def unenroll_target(
self,
target: dict[str, Any],
*,
secret: str,
) -> dict[str, Any]:
"""Compensate an untouched target through the bootstrap boundary only."""
return self._request(
"POST",
"/v1/appops/enrollments/rollback",
secret=secret,
payload={"target": target},
)
def status(self, request_id: str, *, secret: str) -> dict[str, Any]:
if not _REQUEST_ID.fullmatch(request_id):
return {"status": "invalid", "reason": "invalid_broker_request_id"}
return self._request(
"GET",
f"/v1/appops/deployments/{request_id}",
secret=secret,
)
def workmesh_promotion_preflight(self, revision: str, *, secret: str) -> dict[str, Any]:
"""Preflight only the fixed WorkMesh product target at one full SHA."""
if not _WORKMESH_REVISION.fullmatch(revision):
return {"status": "invalid", "reason": "invalid_workmesh_revision"}
return self._request(
"POST",
"/v1/appops/workmesh/promotions/preflight",
secret=secret,
payload={"revision": revision},
)
def workmesh_promotion_apply(
self,
revision: str,
*,
secret: str,
preflight_sha256: str,
idempotency_key: str,
) -> dict[str, Any]:
"""Queue a promotion only when its approved preflight digest still matches."""
if not _WORKMESH_REVISION.fullmatch(revision):
return {"status": "invalid", "reason": "invalid_workmesh_revision"}
if not _WORKMESH_PREFLIGHT_SHA256.fullmatch(preflight_sha256):
return {"status": "invalid", "reason": "invalid_workmesh_preflight_digest"}
if not _WORKMESH_IDEMPOTENCY.fullmatch(idempotency_key):
return {"status": "invalid", "reason": "invalid_workmesh_idempotency_key"}
return self._request(
"POST",
"/v1/appops/workmesh/promotions/apply",
secret=secret,
payload={
"revision": revision,
"preflightSha256": preflight_sha256,
"idempotencyKey": idempotency_key,
},
error_response_fields=frozenset({"request", "repository", "revision", "operation"}),
)
def workmesh_promotion_status(self, request_id: str, *, secret: str) -> dict[str, Any]:
"""Read one HMAC-authenticated fixed-target promotion receipt projection."""
return self.status(request_id, secret=secret)
def delivery_status(self) -> dict[str, Any]:
"""Read bounded, secret-free runnerless delivery health from controller.
This endpoint deliberately sends no deployment credential: the
controller publishes only a private-network, read-only projection of
its durable webhook inbox. All mutations keep using the HMAC-bound
typed request surface above.
"""
if not self.is_configured():
return {"status": "missing_config", "reason": self.configuration_error()}
try:
with httpx.Client(
timeout=self.timeout_seconds,
verify=self._verify_option(),
follow_redirects=False,
) as client, client.stream(
"GET",
f"{self.base_url}/v1/gitea/deliveries",
headers={"Accept": "application/json"},
) as response:
if response.status_code != 200:
return {"status": "unavailable", "reason": f"delivery_status_http_{response.status_code}"}
body = bytearray()
for chunk in response.iter_bytes():
body.extend(chunk)
if len(body) > _MAX_DELIVERY_STATUS_BYTES:
return {"status": "unavailable", "reason": "delivery_status_response_too_large"}
except httpx.RequestError as exc:
return {"status": "unreachable", "reason": redact_message(str(exc))}
try:
decoded = json.loads(bytes(body).decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return {"status": "unavailable", "reason": "delivery_status_invalid_response"}
return decoded if isinstance(decoded, dict) else {"status": "unavailable", "reason": "delivery_status_invalid_response"}
def runner_fleet_upgrade_v2(self, *, reason: str) -> dict[str, Any]:
"""Request the one fixed Tower runner-fleet v2 migration.
The root controller derives and verifies the exact protected-main
revision itself. The caller cannot select a script path, runner name,
revision or Docker target.
"""
return self._request(
"POST",
"/v1/appops/runner-fleet/upgrade-v2",
secret=settings.mcp_token or "",
payload={"reason": reason[:1000]},
)
def runner_fleet_specialize(self, *, reason: str) -> dict[str, Any]:
"""Request the fixed protected-source CI specialization migration."""
result = self._request(
"POST", "/v1/appops/runner-fleet/specialize",
secret=settings.mcp_token or "", payload={"reason": reason[:1000]},
request_timeout=httpx.Timeout(connect=5, write=5, pool=5, read=350),
)
# The controller can outlive a lost HTTP response. Never translate a
# transport failure into proof of no host effects or permission to retry.
if result.get("status") == "unreachable":
return {
"status": "unknown", "reason": "runner_specialization_outcome_unknown",
"noMutation": False, "effectsPerformed": None,
}
return result
def runner_statistics_configure(self, *, reason: str) -> dict[str, Any]:
"""Provision the fixed independent read-only ProjectBrain capability."""
return self._request(
"POST", "/v1/appops/runner-statistics/configure",
secret=settings.mcp_token or "", payload={"reason": reason[:1000]},
)
def runner_acceptance(self, *, reason: str, preflight: bool) -> dict[str, Any]:
result = self._request(
"POST", "/v1/appops/runner-acceptance/preflight" if preflight else "/v1/appops/runner-acceptance/quiesce",
secret=settings.mcp_token or "", payload={"reason": reason[:1000]},
request_timeout=httpx.Timeout(connect=5, write=5, pool=5, read=350),
)
if not preflight and result.get("status") in {"unreachable", "error", "rejected"}:
return {"status": "unknown", "noMutation": False, "effectsPerformed": None,
"blockers": ["maintenance_outcome_unknown"]}
return result
def hub_migration_prepare_permissions(self, *, reason: str) -> dict[str, Any]:
"""Prepare only the fixed Hub backup and controller-state permissions."""
return self._request(
"POST", "/v1/appops/hub-migration/prepare-permissions",
secret=settings.mcp_token or "", payload={"reason": reason[:1000]},
)
def runner_fleet_status(self) -> dict[str, Any]:
"""Read the controller's private, bounded CI sandbox projection.
This is deliberately read-only and sends no credential. The controller
derives the payload from fixed runner identities and its private DinD
daemon; AppOps never receives a Docker socket or a cleanup selector.
"""
return self._runner_fleet_read("/v1/runner-fleet")
def runner_fleet_statistics(self) -> dict[str, Any]:
"""Read the persisted profile projection without scheduling work."""
return self._runner_fleet_read("/v1/runner-fleet/statistics", timeout_seconds=min(self.timeout_seconds, 5))
def _runner_fleet_read(self, path: str, *, timeout_seconds: float | None = None) -> dict[str, Any]:
if path not in {"/v1/runner-fleet", "/v1/runner-fleet/statistics"}:
return {"status": "unavailable", "reason": "runner_fleet_path_invalid"}
if not self.is_configured():
return {"status": "missing_config", "reason": self.configuration_error()}
try:
with httpx.Client(
timeout=self.timeout_seconds if timeout_seconds is None else timeout_seconds,
verify=self._verify_option(),
follow_redirects=False,
) as client, client.stream(
"GET",
f"{self.base_url}{path}",
headers={"Accept": "application/json"},
) as response:
if response.status_code != 200:
return {"status": "unavailable", "reason": f"runner_fleet_status_http_{response.status_code}"}
body = bytearray()
for chunk in response.iter_bytes():
body.extend(chunk)
if len(body) > _MAX_RUNNER_FLEET_STATUS_BYTES:
return {"status": "unavailable", "reason": "runner_fleet_status_response_too_large"}
except httpx.RequestError as exc:
return {"status": "unreachable", "reason": redact_message(str(exc))}
try:
decoded = json.loads(bytes(body).decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return {"status": "unavailable", "reason": "runner_fleet_status_invalid_response"}
return decoded if isinstance(decoded, dict) else {"status": "unavailable", "reason": "runner_fleet_status_invalid_response"}