"""Client for the private, typed deployment broker surface. The client only sends a canonical DeploymentPlanV1 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(), ) -> 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=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}: return {"status": "unauthorized", "reason": "deployment_broker_authentication_failed"} 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 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 {"baseline", "preflight", "adopt", "status", "rollback", "delivery"}: raise ValueError("registered legacy stage 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.""" return self._request( "POST", "/v1/appops/runner-fleet/specialize", secret=settings.mcp_token or "", payload={"reason": reason[:1000]}, ) 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 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"}